dd-harness-mcp 0.5.0 → 0.7.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/acentuacao.js +251 -0
- package/dist/cli/src/config.js +18 -0
- package/dist/cli/src/escreve-config.js +101 -0
- package/dist/cli/src/gravar.js +10 -0
- package/dist/cli/src/index.js +167 -6
- package/dist/cli/src/pergunta.js +31 -0
- package/dist/cli/src/politica.js +6 -2
- package/dist/cli/src/projeto.js +11 -0
- package/dist/cli/src/worker.js +28 -8
- package/dist/mcp/src/index.js +58 -49
- package/package.json +1 -1
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recusa conteudo em pt-BR escrito sem acento — o portao, nao um pedido.
|
|
3
|
+
*
|
|
4
|
+
* **Gemeo de `src/lib/acentuacao.ts`, de proposito.** O CLI e publicado no npm com
|
|
5
|
+
* `files: ["dist"]` e sem nenhuma dependencia; importar do app quebraria o pacote
|
|
6
|
+
* instalado, e fazer o app depender do pacote publicavel so para compartilhar uma lista
|
|
7
|
+
* de palavras acopla mais do que resolve. O `tsconfig.build.json` do MCP ja assume essa
|
|
8
|
+
* troca ("duplica algumas funcoes no pacote publicado, e esse e o preco"). Mesma escolha
|
|
9
|
+
* de `recusaAncoraInvalida`, que tambem espelha de proposito um CHECK do banco: a API e a
|
|
10
|
+
* autoridade, isto aqui so adianta o erro antes da rede. **Mudou a lista aqui, mude la.**
|
|
11
|
+
*
|
|
12
|
+
* **Por que isto existe.** Acento entra no embedding. Medido com
|
|
13
|
+
* `text-embedding-3-small` nas frases reais do acervo, a mesma sentenca com e sem acento
|
|
14
|
+
* da ~0,94 de similaridade: 6% de perda. Sozinho nao seria fatal; o que quebra a busca e
|
|
15
|
+
* a **assimetria** — com metade do acervo acentuado e metade nao, o vizinho mais proximo
|
|
16
|
+
* passa a ser quem casa com a GRAFIA da consulta, nao quem e mais relevante. A busca nao
|
|
17
|
+
* devolve erro: devolve resultado ruim com aparencia de bom, que e a falha que este
|
|
18
|
+
* projeto mais combate (mesma razao de `provedor.ts` recusar vetor de outro provedor).
|
|
19
|
+
*
|
|
20
|
+
* **Por que recusar em vez de corrigir.** Restaurar acento por heuristica erra: `e`/`é`,
|
|
21
|
+
* `esta`/`está`, `secretaria`/`secretária` dependem de sintaxe, nao de ortografia. Corrigir
|
|
22
|
+
* gravaria conteudo adulterado em silencio — pior que recusar, porque ninguem revisa.
|
|
23
|
+
*
|
|
24
|
+
* **Por que lista de palavras e nao dicionario.** Sem dependencia nova, e o alvo e o caso
|
|
25
|
+
* real observado: o texto INTEIRO em ASCII. Codigo, termo em ingles e nome proprio nao
|
|
26
|
+
* disparam porque nao estao na lista.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Palavras cuja grafia correta em pt-BR SEMPRE leva acento — nao existe homografo sem
|
|
30
|
+
* acento que as torne ambiguas.
|
|
31
|
+
*
|
|
32
|
+
* Criterio de entrada nesta lista: a forma sem acento **nao e palavra** do portugues.
|
|
33
|
+
* Por isso `e` (de `é`), `esta` (de `está`), `so` (de `só`), `ate` (de `até`) ficam de
|
|
34
|
+
* FORA por mais comuns que sejam — `e`, `esta` e `so` existem sozinhas, e acusar um texto
|
|
35
|
+
* legitimo e pior do que deixar um passar. A lista nao precisa ser exaustiva: basta pegar
|
|
36
|
+
* texto corrido em pt-BR, e um paragrafo de verdade quase sempre tem varias destas.
|
|
37
|
+
*/
|
|
38
|
+
const SEMPRE_ACENTUADAS = new Map([
|
|
39
|
+
["nao", "não"],
|
|
40
|
+
["sao", "são"],
|
|
41
|
+
["tambem", "também"],
|
|
42
|
+
["entao", "então"],
|
|
43
|
+
["porem", "porém"],
|
|
44
|
+
["alem", "além"],
|
|
45
|
+
["memoria", "memória"],
|
|
46
|
+
["memorias", "memórias"],
|
|
47
|
+
["codigo", "código"],
|
|
48
|
+
["funcao", "função"],
|
|
49
|
+
["funcoes", "funções"],
|
|
50
|
+
["versao", "versão"],
|
|
51
|
+
["versoes", "versões"],
|
|
52
|
+
["validacao", "validação"],
|
|
53
|
+
["informacao", "informação"],
|
|
54
|
+
["informacoes", "informações"],
|
|
55
|
+
["aplicacao", "aplicação"],
|
|
56
|
+
["configuracao", "configuração"],
|
|
57
|
+
["implementacao", "implementação"],
|
|
58
|
+
["decisao", "decisão"],
|
|
59
|
+
["decisoes", "decisões"],
|
|
60
|
+
["excecao", "exceção"],
|
|
61
|
+
["excecoes", "exceções"],
|
|
62
|
+
["padrao", "padrão"],
|
|
63
|
+
["usuario", "usuário"],
|
|
64
|
+
["usuarios", "usuários"],
|
|
65
|
+
["necessario", "necessário"],
|
|
66
|
+
["obrigatorio", "obrigatório"],
|
|
67
|
+
["proprio", "próprio"],
|
|
68
|
+
["propria", "própria"],
|
|
69
|
+
["publico", "público"],
|
|
70
|
+
["publica", "pública"],
|
|
71
|
+
["unico", "único"],
|
|
72
|
+
["unica", "única"],
|
|
73
|
+
["basico", "básico"],
|
|
74
|
+
["automatico", "automático"],
|
|
75
|
+
["historico", "histórico"],
|
|
76
|
+
["numero", "número"],
|
|
77
|
+
["numeros", "números"],
|
|
78
|
+
["calculo", "cálculo"],
|
|
79
|
+
["metodo", "método"],
|
|
80
|
+
["modulo", "módulo"],
|
|
81
|
+
["servico", "serviço"],
|
|
82
|
+
["servicos", "serviços"],
|
|
83
|
+
["referencia", "referência"],
|
|
84
|
+
["consequencia", "consequência"],
|
|
85
|
+
["dependencia", "dependência"],
|
|
86
|
+
["experiencia", "experiência"],
|
|
87
|
+
["sequencia", "sequência"],
|
|
88
|
+
["transacao", "transação"],
|
|
89
|
+
["sessao", "sessão"],
|
|
90
|
+
["expiracao", "expiração"],
|
|
91
|
+
["precisao", "precisão"],
|
|
92
|
+
["preco", "preço"],
|
|
93
|
+
["precos", "preços"],
|
|
94
|
+
["cabecalho", "cabeçalho"],
|
|
95
|
+
["comeca", "começa"],
|
|
96
|
+
["comecar", "começar"],
|
|
97
|
+
["criterio", "critério"],
|
|
98
|
+
["criterios", "critérios"],
|
|
99
|
+
["dominio", "domínio"],
|
|
100
|
+
["indice", "índice"],
|
|
101
|
+
["minimo", "mínimo"],
|
|
102
|
+
["maximo", "máximo"],
|
|
103
|
+
["ultimo", "último"],
|
|
104
|
+
["proximo", "próximo"],
|
|
105
|
+
["disponivel", "disponível"],
|
|
106
|
+
["possivel", "possível"],
|
|
107
|
+
["impossivel", "impossível"],
|
|
108
|
+
["responsavel", "responsável"],
|
|
109
|
+
["nivel", "nível"],
|
|
110
|
+
["util", "útil"],
|
|
111
|
+
["dificil", "difícil"],
|
|
112
|
+
["facil", "fácil"],
|
|
113
|
+
["ja", "já"],
|
|
114
|
+
["voce", "você"],
|
|
115
|
+
["ambiguo", "ambíguo"],
|
|
116
|
+
["generico", "genérico"],
|
|
117
|
+
["especifico", "específico"],
|
|
118
|
+
["logica", "lógica"],
|
|
119
|
+
["pagina", "página"],
|
|
120
|
+
["paginas", "páginas"],
|
|
121
|
+
["orfao", "órfão"],
|
|
122
|
+
["apos", "após"],
|
|
123
|
+
["atraves", "através"],
|
|
124
|
+
["representacao", "representação"],
|
|
125
|
+
["representacoes", "representações"],
|
|
126
|
+
["operacao", "operação"],
|
|
127
|
+
["operacoes", "operações"],
|
|
128
|
+
["integracao", "integração"],
|
|
129
|
+
["migracao", "migração"],
|
|
130
|
+
["autenticacao", "autenticação"],
|
|
131
|
+
["autorizacao", "autorização"],
|
|
132
|
+
["indexacao", "indexação"],
|
|
133
|
+
["condicao", "condição"],
|
|
134
|
+
["condicoes", "condições"],
|
|
135
|
+
["relacao", "relação"],
|
|
136
|
+
["situacao", "situação"],
|
|
137
|
+
["definicao", "definição"],
|
|
138
|
+
["descricao", "descrição"],
|
|
139
|
+
["restricao", "restrição"],
|
|
140
|
+
["restricoes", "restrições"],
|
|
141
|
+
["execucao", "execução"],
|
|
142
|
+
["atencao", "atenção"],
|
|
143
|
+
["intencao", "intenção"],
|
|
144
|
+
["manutencao", "manutenção"],
|
|
145
|
+
["observacao", "observação"],
|
|
146
|
+
["alteracao", "alteração"],
|
|
147
|
+
["criacao", "criação"],
|
|
148
|
+
["remocao", "remoção"],
|
|
149
|
+
["gravacao", "gravação"],
|
|
150
|
+
["duplicacao", "duplicação"],
|
|
151
|
+
["comparacao", "comparação"],
|
|
152
|
+
["verificacao", "verificação"],
|
|
153
|
+
["conteudo", "conteúdo"],
|
|
154
|
+
["politica", "política"],
|
|
155
|
+
["politicas", "políticas"],
|
|
156
|
+
["estrategia", "estratégia"],
|
|
157
|
+
["tecnico", "técnico"],
|
|
158
|
+
["tecnica", "técnica"],
|
|
159
|
+
["semantica", "semântica"],
|
|
160
|
+
["semantico", "semântico"],
|
|
161
|
+
["parametro", "parâmetro"],
|
|
162
|
+
["parametros", "parâmetros"],
|
|
163
|
+
["variavel", "variável"],
|
|
164
|
+
["variaveis", "variáveis"],
|
|
165
|
+
["multiplo", "múltiplo"],
|
|
166
|
+
["multiplos", "múltiplos"],
|
|
167
|
+
["multipla", "múltipla"],
|
|
168
|
+
["invalido", "inválido"],
|
|
169
|
+
["invalida", "inválida"],
|
|
170
|
+
["valido", "válido"],
|
|
171
|
+
["obrigatoria", "obrigatória"],
|
|
172
|
+
["tres", "três"],
|
|
173
|
+
["proposito", "propósito"],
|
|
174
|
+
["cenario", "cenário"],
|
|
175
|
+
["relatorio", "relatório"],
|
|
176
|
+
["diretorio", "diretório"],
|
|
177
|
+
["repositorio", "repositório"],
|
|
178
|
+
["formulario", "formulário"],
|
|
179
|
+
["comentario", "comentário"],
|
|
180
|
+
["comentarios", "comentários"],
|
|
181
|
+
["glossario", "glossário"],
|
|
182
|
+
["saidas", "saídas"],
|
|
183
|
+
["duvida", "dúvida"],
|
|
184
|
+
["duvidas", "dúvidas"],
|
|
185
|
+
["ausencia", "ausência"],
|
|
186
|
+
["presenca", "presença"],
|
|
187
|
+
["diferenca", "diferença"],
|
|
188
|
+
["diferencas", "diferenças"],
|
|
189
|
+
["seguranca", "segurança"],
|
|
190
|
+
["licenca", "licença"],
|
|
191
|
+
["mudanca", "mudança"],
|
|
192
|
+
["mudancas", "mudanças"],
|
|
193
|
+
["heranca", "herança"],
|
|
194
|
+
["cabecalhos", "cabeçalhos"],
|
|
195
|
+
["comecam", "começam"],
|
|
196
|
+
["sera", "será"],
|
|
197
|
+
["serao", "serão"],
|
|
198
|
+
]);
|
|
199
|
+
/**
|
|
200
|
+
* Devolve as palavras que exigem acento e apareceram sem ele.
|
|
201
|
+
*
|
|
202
|
+
* Percorre o texto ja em minusculas e sem os trechos onde ASCII e legitimo: bloco de
|
|
203
|
+
* codigo (``` ... ```), codigo inline (`...`), URL e caminho de arquivo. Sem isso, um
|
|
204
|
+
* `preco_centavos` no meio de um identificador acusaria prosa que esta correta.
|
|
205
|
+
*/
|
|
206
|
+
export function palavrasSemAcento(texto) {
|
|
207
|
+
const limpo = texto
|
|
208
|
+
.replace(/```[\s\S]*?```/g, " ")
|
|
209
|
+
.replace(/`[^`\n]*`/g, " ")
|
|
210
|
+
.replace(/\bhttps?:\/\/\S+/gi, " ")
|
|
211
|
+
.replace(/\b[\w./-]+\.(?:ts|tsx|js|mjs|json|md|sql|css|html|py|sh)\b/gi, " ")
|
|
212
|
+
.toLowerCase();
|
|
213
|
+
const achadas = new Map();
|
|
214
|
+
for (const bruta of limpo.split(/[^a-zà-ÿ]+/)) {
|
|
215
|
+
if (!bruta)
|
|
216
|
+
continue;
|
|
217
|
+
const correto = SEMPRE_ACENTUADAS.get(bruta);
|
|
218
|
+
if (correto)
|
|
219
|
+
achadas.set(bruta, correto);
|
|
220
|
+
}
|
|
221
|
+
return [...achadas].map(([escrito, correto]) => ({ escrito, correto }));
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* A mensagem de recusa, ou `null` se o texto passa.
|
|
225
|
+
*
|
|
226
|
+
* Recebe os campos ja separados para poder dizer ONDE esta o problema — quem escreveu o
|
|
227
|
+
* corpo inteiro sem acento nao quer procurar a palavra em 40 linhas.
|
|
228
|
+
*/
|
|
229
|
+
export function recusaPorAcentuacao(campos) {
|
|
230
|
+
const porCampo = campos
|
|
231
|
+
.map(({ campo, texto }) => ({ campo, suspeitas: palavrasSemAcento(texto) }))
|
|
232
|
+
.filter((c) => c.suspeitas.length > 0);
|
|
233
|
+
if (porCampo.length === 0)
|
|
234
|
+
return null;
|
|
235
|
+
const detalhe = porCampo
|
|
236
|
+
.map(({ campo, suspeitas }) => {
|
|
237
|
+
const amostra = suspeitas
|
|
238
|
+
.slice(0, 5)
|
|
239
|
+
.map((s) => `${s.escrito} → ${s.correto}`)
|
|
240
|
+
.join(", ");
|
|
241
|
+
const resto = suspeitas.length > 5 ? `, e mais ${suspeitas.length - 5}` : "";
|
|
242
|
+
return ` ${campo}: ${amostra}${resto}`;
|
|
243
|
+
})
|
|
244
|
+
.join("\n");
|
|
245
|
+
return ("conteúdo em pt-BR sem acentuação. A acentuação entra no embedding: a mesma frase " +
|
|
246
|
+
"sem acento cai para ~0,94 de similaridade, e um acervo com grafia misturada faz a " +
|
|
247
|
+
"busca premiar quem escreve igual à consulta em vez de quem é relevante.\n" +
|
|
248
|
+
`${detalhe}\n` +
|
|
249
|
+
"Reescreva com a acentuação correta — não é corrigido automaticamente porque " +
|
|
250
|
+
"adivinhar entre `e`/`é` e `esta`/`está` gravaria texto adulterado em silêncio.");
|
|
251
|
+
}
|
package/dist/cli/src/config.js
CHANGED
|
@@ -31,6 +31,24 @@ export async function leConfigDoRepo(raiz) {
|
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
33
|
const arquivoDeCredenciais = () => join(homedir(), ".dd-harness", "credentials.json");
|
|
34
|
+
const arquivoDeConfigDaMaquina = () => join(homedir(), ".dd-harness", "config.json");
|
|
35
|
+
export async function leConfigDaMaquina() {
|
|
36
|
+
try {
|
|
37
|
+
const cru = await readFile(arquivoDeConfigDaMaquina(), "utf8");
|
|
38
|
+
return JSON.parse(cru);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export async function guardaConfigDaMaquina(parcial) {
|
|
45
|
+
const caminho = arquivoDeConfigDaMaquina();
|
|
46
|
+
await mkdir(join(homedir(), ".dd-harness"), { recursive: true });
|
|
47
|
+
const atual = await leConfigDaMaquina();
|
|
48
|
+
const mesclado = { ...atual, ...parcial };
|
|
49
|
+
await writeFile(caminho, `${JSON.stringify(mesclado, null, 2)}\n`, "utf8");
|
|
50
|
+
return caminho;
|
|
51
|
+
}
|
|
34
52
|
/**
|
|
35
53
|
* Credencial por origem da API: a mesma maquina pode falar com uma instalacao local e
|
|
36
54
|
* com a de producao, e misturar token entre as duas daria 401 confuso.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
const COMANDO_DO_HOOK = "dd-harness politica --hook";
|
|
4
|
+
/**
|
|
5
|
+
* Acrescenta o hook do dd-harness a `.claude/settings.json`, sem tocar em outros hooks.
|
|
6
|
+
*
|
|
7
|
+
* `SessionStart` e um ARRAY de matcher groups no schema do Claude Code — pensado para
|
|
8
|
+
* varios hooks convivendo. Por isso a acao certa quando ja existe outro hook de
|
|
9
|
+
* SessionStart e ACRESCENTAR um grupo novo ao array, nao substituir nem perguntar: sao
|
|
10
|
+
* dois hooks independentes, cada um cuidando da sua coisa.
|
|
11
|
+
*/
|
|
12
|
+
export async function escreveHook(raiz) {
|
|
13
|
+
const caminho = join(raiz, ".claude", "settings.json");
|
|
14
|
+
let doc = {};
|
|
15
|
+
try {
|
|
16
|
+
const cru = await readFile(caminho, "utf8");
|
|
17
|
+
doc = JSON.parse(cru);
|
|
18
|
+
}
|
|
19
|
+
catch (erro) {
|
|
20
|
+
if (erro.code !== "ENOENT") {
|
|
21
|
+
return { ok: false, motivo: "json-invalido", caminho };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const hooks = (doc.hooks ??= {});
|
|
25
|
+
const sessionStart = (hooks.SessionStart ??= []);
|
|
26
|
+
const jaTemONosso = JSON.stringify(sessionStart).includes(COMANDO_DO_HOOK);
|
|
27
|
+
if (jaTemONosso)
|
|
28
|
+
return { ok: true, estado: "ja-tinha" };
|
|
29
|
+
const eraVazio = sessionStart.length === 0;
|
|
30
|
+
sessionStart.push({
|
|
31
|
+
hooks: [
|
|
32
|
+
{
|
|
33
|
+
type: "command",
|
|
34
|
+
command: COMANDO_DO_HOOK,
|
|
35
|
+
statusMessage: "Carregando a política do dd-harness...",
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
await mkdir(dirname(caminho), { recursive: true });
|
|
40
|
+
await writeFile(caminho, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
|
|
41
|
+
return { ok: true, estado: eraVazio ? "criado" : "acrescentado" };
|
|
42
|
+
}
|
|
43
|
+
/** Acrescenta o servidor MCP a `.mcp.json`, sem tocar em outros servidores. */
|
|
44
|
+
export async function escreveMcp(raiz) {
|
|
45
|
+
const caminho = join(raiz, ".mcp.json");
|
|
46
|
+
let doc = {};
|
|
47
|
+
try {
|
|
48
|
+
const cru = await readFile(caminho, "utf8");
|
|
49
|
+
doc = JSON.parse(cru);
|
|
50
|
+
}
|
|
51
|
+
catch (erro) {
|
|
52
|
+
if (erro.code !== "ENOENT") {
|
|
53
|
+
return { ok: false, motivo: "json-invalido", caminho };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const servidores = (doc.mcpServers ??= {});
|
|
57
|
+
if (servidores["dd-harness"])
|
|
58
|
+
return { ok: true, estado: "ja-tinha" };
|
|
59
|
+
const eraVazio = Object.keys(servidores).length === 0;
|
|
60
|
+
servidores["dd-harness"] = { command: "npx", args: ["-y", "dd-harness-mcp"] };
|
|
61
|
+
await mkdir(dirname(caminho), { recursive: true });
|
|
62
|
+
await writeFile(caminho, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
|
|
63
|
+
return { ok: true, estado: eraVazio ? "criado" : "acrescentado" };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* `AGENTS.md` e markdown livre, nao JSON — nao ha "mesclar" de verdade. Se o arquivo ja
|
|
67
|
+
* existe (com qualquer conteudo), so acrescenta a instrucao no fim; nunca sobrescreve
|
|
68
|
+
* o que a pessoa escreveu, porque markdown de outra ferramenta e mais dificil de separar
|
|
69
|
+
* do nosso do que uma chave de objeto.
|
|
70
|
+
*/
|
|
71
|
+
export async function escreveAgents(raiz) {
|
|
72
|
+
const caminho = join(raiz, "AGENTS.md");
|
|
73
|
+
const instrucao = SUGESTAO_AGENTS;
|
|
74
|
+
let atual = null;
|
|
75
|
+
try {
|
|
76
|
+
atual = await readFile(caminho, "utf8");
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Sem arquivo: cria do zero.
|
|
80
|
+
}
|
|
81
|
+
if (atual === null) {
|
|
82
|
+
await writeFile(caminho, `${instrucao}\n`, "utf8");
|
|
83
|
+
return { estado: "criado" };
|
|
84
|
+
}
|
|
85
|
+
if (atual.includes("ler_artefato"))
|
|
86
|
+
return { estado: "ja-tinha" };
|
|
87
|
+
const separador = atual.endsWith("\n") ? "\n" : "\n\n";
|
|
88
|
+
await writeFile(caminho, `${atual}${separador}${instrucao}\n`, "utf8");
|
|
89
|
+
return { estado: "acrescentado" };
|
|
90
|
+
}
|
|
91
|
+
const SUGESTAO_AGENTS = `## Protocolo do dd-harness
|
|
92
|
+
|
|
93
|
+
Este projeto tem um protocolo de trabalho que **não está neste arquivo**.
|
|
94
|
+
|
|
95
|
+
**ANTES de qualquer outra coisa**, chame a ferramenta MCP \`ler_artefato\` com
|
|
96
|
+
\`tipo: "politica"\` e siga o que ela devolver. É a primeira ação da sessão, antes
|
|
97
|
+
de ler código, responder ou planejar.
|
|
98
|
+
|
|
99
|
+
- Se a ferramenta não existir, o servidor MCP do dd-harness não está declarado:
|
|
100
|
+
avise o usuário e **não modifique nada** até ele resolver.
|
|
101
|
+
- Se ela devolver vazio, este projeto nunca foi briefado — diga isso ao usuário.`;
|
package/dist/cli/src/gravar.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { recusaPorAcentuacao } from "./acentuacao.js";
|
|
2
3
|
import { cabecalhos, credencial, pede, recusa } from "./api.js";
|
|
3
4
|
const OBRIGATORIOS = ["name", "titulo", "description", "pasta"];
|
|
4
5
|
/**
|
|
@@ -135,6 +136,15 @@ export function interpreta(texto) {
|
|
|
135
136
|
.trim();
|
|
136
137
|
if (!corpo)
|
|
137
138
|
throw new Error("o corpo da memória está vazio.");
|
|
139
|
+
// A acentuacao entra no embedding, entao grafia misturada degrada a busca de todo o
|
|
140
|
+
// acervo. A API recusa igual; aqui o erro chega antes da rede e ja aponta a palavra.
|
|
141
|
+
const semAcento = recusaPorAcentuacao([
|
|
142
|
+
{ campo: "titulo", texto: campos.get("titulo") },
|
|
143
|
+
{ campo: "description", texto: campos.get("description") },
|
|
144
|
+
{ campo: "corpo", texto: corpo },
|
|
145
|
+
]);
|
|
146
|
+
if (semAcento)
|
|
147
|
+
throw new Error(semAcento);
|
|
138
148
|
const cauda = resto.slice(corte);
|
|
139
149
|
const tambem = campos.get("projetos") ?? "";
|
|
140
150
|
return {
|
package/dist/cli/src/index.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { access } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
2
5
|
import { termosDaConsulta } from "./argv.js";
|
|
3
6
|
import { check, status } from "./check.js";
|
|
4
|
-
import {
|
|
7
|
+
import { CAMINHO_CONFIG, guardaConfigDaMaquina, guardaToken, leConfigDaMaquina, leConfigDoRepo, leToken, } from "./config.js";
|
|
8
|
+
import { escreveAgents, escreveHook, escreveMcp } from "./escreve-config.js";
|
|
5
9
|
import { grava } from "./gravar.js";
|
|
6
10
|
import { init, SUGESTAO_AGENTS, SUGESTAO_HOOK, SUGESTAO_MCP } from "./init.js";
|
|
11
|
+
import { pergunta, escolha, fechaPerguntas } from "./pergunta.js";
|
|
7
12
|
import { buscaPolitica } from "./politica.js";
|
|
8
13
|
import { busca } from "./buscar.js";
|
|
9
14
|
import { arquiva, edita, le } from "./curar.js";
|
|
10
15
|
import { criaPasta } from "./pasta.js";
|
|
11
|
-
import { criaProjeto } from "./projeto.js";
|
|
16
|
+
import { criaProjeto, listaTenants } from "./projeto.js";
|
|
12
17
|
import { reancora } from "./reancorar.js";
|
|
13
18
|
import { subiuOWorker } from "./worker.js";
|
|
14
19
|
/**
|
|
@@ -21,6 +26,8 @@ import { subiuOWorker } from "./worker.js";
|
|
|
21
26
|
*/
|
|
22
27
|
const AJUDA = `dd-harness — a política e o Brain do projeto, no serviço
|
|
23
28
|
|
|
29
|
+
dd-harness start numa pasta vazia: conduz tudo (login,
|
|
30
|
+
espaço, projeto, config) numa tacada
|
|
24
31
|
dd-harness login --token <token> [--api <url>]
|
|
25
32
|
guarda a credencial desta máquina
|
|
26
33
|
dd-harness projeto <slug> --nome "<nome>" [--tenant <t>] [--api <url>]
|
|
@@ -71,6 +78,142 @@ function argumento(argv, nome) {
|
|
|
71
78
|
const i = argv.indexOf(`--${nome}`);
|
|
72
79
|
return i >= 0 ? argv[i + 1] : undefined;
|
|
73
80
|
}
|
|
81
|
+
const API_PADRAO = "https://dd-harness.vercel.app";
|
|
82
|
+
/**
|
|
83
|
+
* Tenta abrir o navegador padrao. Nunca lanca — falhar em abrir nao pode travar o
|
|
84
|
+
* wizard, so degrada para "aqui esta a URL, abra voce mesmo".
|
|
85
|
+
*/
|
|
86
|
+
function tentaAbrirNavegador(url) {
|
|
87
|
+
const comando = process.platform === "win32" ? "start" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
88
|
+
try {
|
|
89
|
+
spawn(comando, process.platform === "win32" ? ["", url] : [url], {
|
|
90
|
+
shell: process.platform === "win32",
|
|
91
|
+
stdio: "ignore",
|
|
92
|
+
detached: true,
|
|
93
|
+
}).unref();
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Segue sem navegador — a URL impressa antes desta chamada ja resolve.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* `dd-harness start` — o wizard para pasta vazia.
|
|
101
|
+
*
|
|
102
|
+
* Ate aqui, comecar um projeto exigia saber de memoria a sequencia certa
|
|
103
|
+
* (`projeto` -> `init` -> copiar tres blocos de JSON) e o slug do tenant. `start`
|
|
104
|
+
* conduz tudo numa tacada, perguntando so o que falta.
|
|
105
|
+
*
|
|
106
|
+
* Reverte "sugerir, nunca escrever" para `.claude/settings.json` e `.mcp.json` — decisao
|
|
107
|
+
* tomada de proposito aqui, ao contrario de `init`, que continua so sugerindo. O preco e
|
|
108
|
+
* o merge cuidadoso em `escreve-config.ts`: nunca perder o que a pessoa ja tinha.
|
|
109
|
+
*/
|
|
110
|
+
async function comandoStart() {
|
|
111
|
+
const caminhoConfig = join(process.cwd(), CAMINHO_CONFIG);
|
|
112
|
+
try {
|
|
113
|
+
await access(caminhoConfig);
|
|
114
|
+
throw new Error(`este repositório já tem ${CAMINHO_CONFIG} — start é só para pasta vazia. ` +
|
|
115
|
+
"Use `dd-harness init` para ajustar um projeto já existente.");
|
|
116
|
+
}
|
|
117
|
+
catch (erro) {
|
|
118
|
+
if (erro instanceof Error && erro.message.includes("já tem"))
|
|
119
|
+
throw erro;
|
|
120
|
+
// ENOENT: exatamente o caso esperado, segue.
|
|
121
|
+
}
|
|
122
|
+
console.log("Iniciando o dd-harness nesta pasta.\n");
|
|
123
|
+
// 1. Credencial. Sem ela nada do resto e possivel — nem listar tenant.
|
|
124
|
+
let token = await leToken(API_PADRAO);
|
|
125
|
+
if (!token) {
|
|
126
|
+
console.log(`Sem credencial salva para ${API_PADRAO}.`);
|
|
127
|
+
token = await pergunta("Cole o token pessoal (crie um em /tokens na interface): ");
|
|
128
|
+
await guardaToken(API_PADRAO, token);
|
|
129
|
+
console.log("Credencial guardada.\n");
|
|
130
|
+
}
|
|
131
|
+
// 2. Espaco (tenant). Criar um e ato de dono — nunca por aqui — mas ESCOLHER entre
|
|
132
|
+
// os que ja existem e so leitura, e e o que faltava para nao exigir o slug de memoria.
|
|
133
|
+
let tenants = await listaTenants(API_PADRAO, token);
|
|
134
|
+
while (tenants.length === 0) {
|
|
135
|
+
const url = `${API_PADRAO}/tenants`;
|
|
136
|
+
console.log(`\nVocê ainda não participa de nenhum espaço. Abrindo ${url} ...`);
|
|
137
|
+
tentaAbrirNavegador(url);
|
|
138
|
+
await pergunta("Crie um espaço lá e volte aqui. Pressione Enter para continuar: ");
|
|
139
|
+
tenants = await listaTenants(API_PADRAO, token);
|
|
140
|
+
}
|
|
141
|
+
const tenant = await escolha("\nEscolha um espaço:", tenants.map((t) => ({ rotulo: `${t.nome} (${t.slug})`, valor: t.slug })));
|
|
142
|
+
// 3. Projeto. Criar e subordinado a estar num espaco — isso o CLI ja faz sozinho.
|
|
143
|
+
const nomeSugerido = process.cwd().split(/[\\/]/).pop() ?? "meu-projeto";
|
|
144
|
+
const nome = (await pergunta(`\nNome do projeto [${nomeSugerido}]: `)) || nomeSugerido;
|
|
145
|
+
// Nome so com caracteres especiais (ex: "!!!") normaliza para string vazia — o
|
|
146
|
+
// fallback evita sugerir slug vazio, que o servidor recusaria com erro confuso.
|
|
147
|
+
const slugSugerido = nome
|
|
148
|
+
.toLowerCase()
|
|
149
|
+
.normalize("NFD")
|
|
150
|
+
.replace(/[̀-ͯ]/g, "")
|
|
151
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
152
|
+
.replace(/^-+|-+$/g, "") || "meu-projeto";
|
|
153
|
+
const slugDoProjeto = (await pergunta(`Identificador curto (slug) [${slugSugerido}]: `)) || slugSugerido;
|
|
154
|
+
const resultadoProjeto = await criaProjeto(process.cwd(), slugDoProjeto, nome, {
|
|
155
|
+
tenant,
|
|
156
|
+
api: API_PADRAO,
|
|
157
|
+
});
|
|
158
|
+
console.log(resultadoProjeto.jaExistia
|
|
159
|
+
? `\nProjeto ${resultadoProjeto.projeto} já existia — usando ele.`
|
|
160
|
+
: `\nProjeto ${resultadoProjeto.projeto} criado.`);
|
|
161
|
+
// 4. Config do repositorio — o unico arquivo que `init` tambem escreveria.
|
|
162
|
+
const r = await init(process.cwd(), { tenant, projeto: resultadoProjeto.projeto, api: API_PADRAO });
|
|
163
|
+
console.log(r.config === "criada" ? `criado ${CAMINHO_CONFIG}` : `mantido ${CAMINHO_CONFIG}`);
|
|
164
|
+
// 5. Hook e MCP — ESCRITOS, com merge. E aqui que `start` diverge de `init`.
|
|
165
|
+
const hook = await escreveHook(process.cwd());
|
|
166
|
+
if (!hook.ok) {
|
|
167
|
+
console.log(`\nAVISO: ${hook.caminho} não é um JSON válido — não toquei nele. ` +
|
|
168
|
+
"Acrescente manualmente:\n");
|
|
169
|
+
console.log(SUGESTAO_HOOK);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
console.log({ criado: "criado .claude/settings.json (hook adicionado)",
|
|
173
|
+
"ja-tinha": "mantido .claude/settings.json — hook já estava lá",
|
|
174
|
+
acrescentado: "atualizado .claude/settings.json (hook adicionado ao que já existia)",
|
|
175
|
+
}[hook.estado]);
|
|
176
|
+
}
|
|
177
|
+
const mcp = await escreveMcp(process.cwd());
|
|
178
|
+
if (!mcp.ok) {
|
|
179
|
+
console.log(`\nAVISO: ${mcp.caminho} não é um JSON válido — não toquei nele. ` +
|
|
180
|
+
"Acrescente manualmente:\n");
|
|
181
|
+
console.log(SUGESTAO_MCP);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
console.log({ criado: "criado .mcp.json (servidor dd-harness adicionado)",
|
|
185
|
+
"ja-tinha": "mantido .mcp.json — servidor já estava declarado",
|
|
186
|
+
acrescentado: "atualizado .mcp.json (servidor adicionado ao que já existia)",
|
|
187
|
+
}[mcp.estado]);
|
|
188
|
+
}
|
|
189
|
+
// 6. AGENTS.md — so quem usa outra ferramenta alem do Claude Code precisa.
|
|
190
|
+
const outraFerramenta = await pergunta("\nVai abrir este projeto em Codex, Cursor ou outra ferramenta além do Claude " +
|
|
191
|
+
"Code? [s/N]: ");
|
|
192
|
+
if (/^s/i.test(outraFerramenta)) {
|
|
193
|
+
const agents = await escreveAgents(process.cwd());
|
|
194
|
+
console.log({ criado: "criado AGENTS.md",
|
|
195
|
+
"ja-tinha": "mantido AGENTS.md — já apontava para a política",
|
|
196
|
+
acrescentado: "atualizado AGENTS.md (instrução acrescentada ao que já existia)",
|
|
197
|
+
}[agents.estado]);
|
|
198
|
+
}
|
|
199
|
+
// 7. Onde esta o monorepo do worker nesta maquina? So pergunta uma vez, e so importa
|
|
200
|
+
// se houver memoria na fila agora — pular aqui nao trava nada, so avisa mais vezes.
|
|
201
|
+
const semWorkerConfigurado = !(await temWorkerConfigurado());
|
|
202
|
+
if (semWorkerConfigurado) {
|
|
203
|
+
const caminho = await pergunta("\nO worker de embeddings roda local (não hospedado). Onde está o repositório " +
|
|
204
|
+
"`dd-harness_new` clonado nesta máquina? (Enter para pular — o hook vai " +
|
|
205
|
+
"avisar quando precisar): ");
|
|
206
|
+
if (caminho)
|
|
207
|
+
await guardaConfigDaMaquina({ workerRepo: caminho });
|
|
208
|
+
}
|
|
209
|
+
fechaPerguntas();
|
|
210
|
+
console.log("\nPronto. Abra uma sessão do Claude Code nesta pasta — a política vai ser o " +
|
|
211
|
+
"primeiro assunto.");
|
|
212
|
+
}
|
|
213
|
+
async function temWorkerConfigurado() {
|
|
214
|
+
const { workerRepo } = await leConfigDaMaquina();
|
|
215
|
+
return Boolean(workerRepo);
|
|
216
|
+
}
|
|
74
217
|
async function comandoInit(argv) {
|
|
75
218
|
const tenant = argumento(argv, "tenant");
|
|
76
219
|
const projeto = argumento(argv, "projeto");
|
|
@@ -316,7 +459,9 @@ async function comandoPolitica(argv) {
|
|
|
316
459
|
return;
|
|
317
460
|
}
|
|
318
461
|
if (r.estado === "sem-politica") {
|
|
319
|
-
console.error("Este projeto ainda não tem política —
|
|
462
|
+
console.error("Este projeto ainda não tem política e/ou briefing — os dois são obrigatórios. " +
|
|
463
|
+
"Rode uma sessão com `--hook` (ou converse com o agente): ele vai conduzir o " +
|
|
464
|
+
"briefing agora e escrever os dois.");
|
|
320
465
|
process.exitCode = 3;
|
|
321
466
|
return;
|
|
322
467
|
}
|
|
@@ -345,9 +490,23 @@ function contextoDaSessao(r, worker) {
|
|
|
345
490
|
if (r.estado === "sem-politica") {
|
|
346
491
|
return {
|
|
347
492
|
...base,
|
|
348
|
-
additionalContext: "AVISO DO DD-HARNESS: este projeto existe no serviço mas
|
|
349
|
-
"—
|
|
350
|
-
"
|
|
493
|
+
additionalContext: "AVISO DO DD-HARNESS: este projeto existe no serviço mas política e/ou " +
|
|
494
|
+
"briefing **estão faltando** — os dois são obrigatórios.\n\n" +
|
|
495
|
+
"Isto não é uma falha: é um projeto novo. Antes de implementar qualquer " +
|
|
496
|
+
"coisa, conduza o briefing agora, nesta sessão:\n\n" +
|
|
497
|
+
"1. Se houver um `CLAUDE.md` na raiz deste repositório, leia-o primeiro — o " +
|
|
498
|
+
"que ele já diz é ponto de partida, não é para ser ignorado.\n" +
|
|
499
|
+
"2. Pergunte ao usuário o que o código e o `CLAUDE.md` não revelarem: " +
|
|
500
|
+
"objetivo do projeto, stack principal, restrições e convenções.\n" +
|
|
501
|
+
"3. Mostre o rascunho da política e do briefing e espere a confirmação " +
|
|
502
|
+
"explícita do usuário antes de escrever.\n" +
|
|
503
|
+
"4. Escreva os dois com a ferramenta MCP `escrever_artefato` " +
|
|
504
|
+
"(`tipo: \"politica\"` e `tipo: \"briefing\"`, uma chamada cada). Se essa " +
|
|
505
|
+
"ferramenta não estiver disponível nesta sessão, pare e diga ao usuário que " +
|
|
506
|
+
"o servidor MCP do dd-harness precisa estar conectado para isto.\n" +
|
|
507
|
+
"5. Depois de escrever os dois, sobrescreva o `CLAUDE.md` da raiz com um " +
|
|
508
|
+
"ponteiro curto para a política deste serviço — a verdade passa a morar " +
|
|
509
|
+
"aqui, e o `CLAUDE.md` local nunca mais precisa ser editado à mão." +
|
|
351
510
|
fila,
|
|
352
511
|
};
|
|
353
512
|
}
|
|
@@ -511,6 +670,8 @@ async function principal() {
|
|
|
511
670
|
return;
|
|
512
671
|
}
|
|
513
672
|
switch (comando) {
|
|
673
|
+
case "start":
|
|
674
|
+
return comandoStart();
|
|
514
675
|
case "init":
|
|
515
676
|
return comandoInit(resto);
|
|
516
677
|
case "login":
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
/**
|
|
3
|
+
* Leitura de terminal, so para `dd-harness start`.
|
|
4
|
+
*
|
|
5
|
+
* O resto do CLI e puramente por flag — nao interativo de proposito, porque script e
|
|
6
|
+
* gancho (`post-commit`, CI) nao tem ninguem para responder um prompt. `start` e a
|
|
7
|
+
* excecao deliberada: e o comando de quem esta sentado no terminal, comecando um
|
|
8
|
+
* projeto do zero, sem saber os slugs de memoria.
|
|
9
|
+
*/
|
|
10
|
+
let rl = null;
|
|
11
|
+
function interfaceAtual() {
|
|
12
|
+
return (rl ??= createInterface({ input: process.stdin, output: process.stdout }));
|
|
13
|
+
}
|
|
14
|
+
export async function pergunta(texto) {
|
|
15
|
+
return (await interfaceAtual().question(texto)).trim();
|
|
16
|
+
}
|
|
17
|
+
export function fechaPerguntas() {
|
|
18
|
+
rl?.close();
|
|
19
|
+
rl = null;
|
|
20
|
+
}
|
|
21
|
+
/** Numera as opcoes, pede um numero, devolve o item escolhido. Sem loop de retry: erra, recomeca o comando — start nao e ambiente para validar input ao infinito. */
|
|
22
|
+
export async function escolha(titulo, itens) {
|
|
23
|
+
console.log(titulo);
|
|
24
|
+
itens.forEach((item, i) => console.log(` ${i + 1}) ${item.rotulo}`));
|
|
25
|
+
const resposta = await pergunta("> ");
|
|
26
|
+
const indice = Number(resposta) - 1;
|
|
27
|
+
const escolhido = itens[indice];
|
|
28
|
+
if (!escolhido)
|
|
29
|
+
throw new Error(`escolha um número entre 1 e ${itens.length}.`);
|
|
30
|
+
return escolhido.valor;
|
|
31
|
+
}
|
package/dist/cli/src/politica.js
CHANGED
|
@@ -24,9 +24,13 @@ export async function buscaPolitica(raiz) {
|
|
|
24
24
|
const payload = (await resposta.json());
|
|
25
25
|
const conteudo = payload.politica?.trim();
|
|
26
26
|
const esperandoIndexacao = payload.esperando_indexacao ?? 0;
|
|
27
|
-
//
|
|
28
|
-
|
|
27
|
+
// `briefado` e do SERVICO, nao inferido aqui: exige politica E briefing, os dois
|
|
28
|
+
// obrigatorios. So checar `politica` vazia deixava passar o caso de politica
|
|
29
|
+
// existir sem briefing — a sessao seguia como "ok" com metade do briefing faltando,
|
|
30
|
+
// e nada acionava o fluxo que devia completa-lo.
|
|
31
|
+
if (!conteudo || payload.briefado === false) {
|
|
29
32
|
return { estado: "sem-politica", esperandoIndexacao };
|
|
33
|
+
}
|
|
30
34
|
return { estado: "ok", conteudo, esperandoIndexacao };
|
|
31
35
|
}
|
|
32
36
|
catch (erro) {
|
package/dist/cli/src/projeto.js
CHANGED
|
@@ -37,3 +37,14 @@ export async function criaProjeto(raiz, slug, nome, explicito = {}) {
|
|
|
37
37
|
const { projeto } = (await resposta.json());
|
|
38
38
|
return { projeto, jaExistia: false };
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Os espacos (tenants) que este token alcanca — para `dd-harness start` deixar escolher
|
|
42
|
+
* em vez de exigir o slug de memoria.
|
|
43
|
+
*/
|
|
44
|
+
export async function listaTenants(api, token) {
|
|
45
|
+
const resposta = await pede(`${api}/api/v1/tenants`, { headers: cabecalhos(token) });
|
|
46
|
+
if (!resposta.ok)
|
|
47
|
+
await recusa(resposta);
|
|
48
|
+
const { tenants } = (await resposta.json());
|
|
49
|
+
return tenants;
|
|
50
|
+
}
|
package/dist/cli/src/worker.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { access } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { leConfigDaMaquina } from "./config.js";
|
|
4
5
|
/**
|
|
5
6
|
* Subir o worker de embeddings quando ha fila esperando.
|
|
6
7
|
*
|
|
@@ -14,19 +15,38 @@ import { join } from "node:path";
|
|
|
14
15
|
*
|
|
15
16
|
* - **So sobe quando ha fila.** O hook roda em TODA sessao, inclusive nas que so leem
|
|
16
17
|
* codigo. Subir com a fila vazia gastaria chamada de embedding sem ninguem ter pedido.
|
|
17
|
-
* - **So
|
|
18
|
-
*
|
|
18
|
+
* - **So quando sabemos ONDE esta o worker.** Ele vive no monorepo `dd-harness_new`, nao
|
|
19
|
+
* no repositorio consumidor — que e onde este hook roda na pratica. Achar o monorepo
|
|
20
|
+
* custou uma medicao real: a primeira versao so checava `raiz` (o cwd de quem chamou),
|
|
21
|
+
* entao so funcionava quando o dd-harness era executado de DENTRO do proprio monorepo
|
|
22
|
+
* — nunca no caso real, que e o projeto do usuario noutra pasta.
|
|
19
23
|
*/
|
|
20
|
-
/** O worker
|
|
21
|
-
|
|
24
|
+
/** O worker existe nesta pasta especifica? */
|
|
25
|
+
async function ehOMonorepo(caminho) {
|
|
22
26
|
try {
|
|
23
|
-
await access(join(
|
|
27
|
+
await access(join(caminho, "src", "worker", "indexador.ts"));
|
|
24
28
|
return true;
|
|
25
29
|
}
|
|
26
30
|
catch {
|
|
27
31
|
return false;
|
|
28
32
|
}
|
|
29
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Onde rodar o worker, se soubermos.
|
|
36
|
+
*
|
|
37
|
+
* Prioridade: `~/.dd-harness/config.json` (`workerRepo`, salvo por `dd-harness start` ou
|
|
38
|
+
* `dd-harness worker --configurar`) primeiro — e o caminho que vale em qualquer pasta que
|
|
39
|
+
* o CLI rode. Cai para `raiz` so por compatibilidade com quem já roda de dentro do
|
|
40
|
+
* monorepo sem ter configurado nada.
|
|
41
|
+
*/
|
|
42
|
+
export async function achaOWorker(raiz) {
|
|
43
|
+
const { workerRepo } = await leConfigDaMaquina();
|
|
44
|
+
if (workerRepo && (await ehOMonorepo(workerRepo)))
|
|
45
|
+
return workerRepo;
|
|
46
|
+
if (await ehOMonorepo(raiz))
|
|
47
|
+
return raiz;
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
30
50
|
/**
|
|
31
51
|
* Dispara um lote e devolve na hora — nao espera terminar.
|
|
32
52
|
*
|
|
@@ -41,12 +61,12 @@ export async function temWorkerLocal(raiz) {
|
|
|
41
61
|
export async function subiuOWorker(raiz, esperando) {
|
|
42
62
|
if (esperando <= 0)
|
|
43
63
|
return { subiu: false, motivo: "sem-fila" };
|
|
44
|
-
|
|
64
|
+
const repo = await achaOWorker(raiz);
|
|
65
|
+
if (!repo)
|
|
45
66
|
return { subiu: false, motivo: "sem-worker" };
|
|
46
|
-
}
|
|
47
67
|
try {
|
|
48
68
|
const filho = spawn("npx", ["pnpm@latest", "worker", "--uma-vez"], {
|
|
49
|
-
cwd:
|
|
69
|
+
cwd: repo,
|
|
50
70
|
detached: true,
|
|
51
71
|
stdio: "ignore",
|
|
52
72
|
shell: process.platform === "win32",
|
package/dist/mcp/src/index.js
CHANGED
|
@@ -34,20 +34,25 @@ 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
|
+
/**
|
|
43
|
+
* A regra e recusada pelo CLI e pela API — esta descricao existe para o agente acertar de
|
|
44
|
+
* primeira, em vez de descobrir no erro.
|
|
45
|
+
*/
|
|
46
|
+
const ACENTUACAO = `Escreva em pt-BR com acentuação correta — título, resumo e corpo. Isto NÃO é estilo: o texto vira embedding, e a mesma frase sem acento cai para ~0,94 de similaridade. Com o acervo em grafia misturada, a busca passa a premiar quem escreve igual à consulta em vez de quem é relevante. Texto sem acento é RECUSADO na gravação; nada é corrigido automaticamente, porque adivinhar entre \`e\`/\`é\` gravaria conteúdo adulterado em silêncio.`;
|
|
42
47
|
function criaServidor() {
|
|
43
48
|
const server = new McpServer({ name: "dd-harness", version: "0.1.0" });
|
|
44
49
|
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, 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
|
-
|
|
50
|
+
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?").
|
|
51
|
+
|
|
52
|
+
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.
|
|
53
|
+
|
|
54
|
+
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.
|
|
55
|
+
|
|
51
56
|
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
57
|
inputSchema: z.object({
|
|
53
58
|
consulta: z
|
|
@@ -81,15 +86,17 @@ Os primeiros achados são os que valem: o piso barra tema alheio, mas num Brain
|
|
|
81
86
|
}
|
|
82
87
|
});
|
|
83
88
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
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.
|
|
90
|
+
|
|
91
|
+
${FILTROS}
|
|
92
|
+
|
|
93
|
+
${ACENTUACAO}
|
|
94
|
+
|
|
95
|
+
A pasta precisa existir antes — use \`criar_pasta\`. Essa recusa é deliberada: ela impede um typo virar pasta nova em silêncio.
|
|
96
|
+
|
|
97
|
+
Â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:
|
|
98
|
+
|
|
99
|
+
- \`src/api/encurtar.js\` — o arquivo inteiro. Use quando a memória fala do arquivo como um todo.
|
|
93
100
|
- \`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
101
|
inputSchema: z.object({
|
|
95
102
|
arquivo: z
|
|
@@ -108,10 +115,10 @@ A pasta precisa existir antes — use \`criar_pasta\`. Essa recusa é deliberada
|
|
|
108
115
|
}
|
|
109
116
|
});
|
|
110
117
|
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
|
-
|
|
118
|
+
description: `Devolve uma memória inteira — corpo, filtros e âncoras — pelo endereço \`<pasta>/<slug>\`.
|
|
119
|
+
|
|
120
|
+
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.
|
|
121
|
+
|
|
115
122
|
Também é o ponto de partida obrigatório de \`editar_memoria\`: o formato devolvido é o mesmo que ela consome.`,
|
|
116
123
|
inputSchema: z.object({
|
|
117
124
|
endereco: z
|
|
@@ -128,11 +135,13 @@ Também é o ponto de partida obrigatório de \`editar_memoria\`: o formato devo
|
|
|
128
135
|
}
|
|
129
136
|
});
|
|
130
137
|
server.registerTool("editar_memoria", {
|
|
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
|
-
|
|
135
|
-
Os três filtros continuam valendo na edição — o banco recusa igual. ${FILTROS}
|
|
138
|
+
description: `Corrige uma memória que já existe, pelo mesmo formato markdown de \`gravar_memoria\`. O endereço sai do frontmatter (pasta + name).
|
|
139
|
+
|
|
140
|
+
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.
|
|
141
|
+
|
|
142
|
+
Os três filtros continuam valendo na edição — o banco recusa igual. ${FILTROS}
|
|
143
|
+
|
|
144
|
+
${ACENTUACAO}`,
|
|
136
145
|
inputSchema: z.object({
|
|
137
146
|
arquivo: z
|
|
138
147
|
.string()
|
|
@@ -149,8 +158,8 @@ Os três filtros continuam valendo na edição — o banco recusa igual. ${FILTR
|
|
|
149
158
|
}
|
|
150
159
|
});
|
|
151
160
|
server.registerTool("arquivar_memoria", {
|
|
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
|
-
|
|
161
|
+
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.
|
|
162
|
+
|
|
154
163
|
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.`,
|
|
155
164
|
inputSchema: z.object({
|
|
156
165
|
endereco: z
|
|
@@ -176,12 +185,12 @@ Use \`substituida_por\` quando outra memória toma o lugar desta — a troca aco
|
|
|
176
185
|
}
|
|
177
186
|
});
|
|
178
187
|
server.registerTool("criar_projeto", {
|
|
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
|
-
|
|
188
|
+
description: `Cria um projeto no serviço — o espaço onde as pastas e memórias deste repositório vão morar.
|
|
189
|
+
|
|
190
|
+
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.
|
|
191
|
+
|
|
192
|
+
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.
|
|
193
|
+
|
|
185
194
|
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.`,
|
|
186
195
|
inputSchema: z.object({
|
|
187
196
|
slug: z
|
|
@@ -206,10 +215,10 @@ O ESPAÇO (tenant) não se cria por aqui, de propósito: ele é a fronteira de i
|
|
|
206
215
|
}
|
|
207
216
|
});
|
|
208
217
|
server.registerTool("criar_pasta", {
|
|
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
|
-
|
|
218
|
+
description: `Cria uma pasta temática no Brain. \`gravar_memoria\` recusa quando a pasta não existe, e esta é a saída.
|
|
219
|
+
|
|
220
|
+
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.
|
|
221
|
+
|
|
213
222
|
Criar uma que já existe não é erro: devolve que já existia, sem alterar a definição.`,
|
|
214
223
|
inputSchema: z.object({
|
|
215
224
|
slug: z
|
|
@@ -233,11 +242,11 @@ Criar uma que já existe não é erro: devolve que já existia, sem alterar a de
|
|
|
233
242
|
}
|
|
234
243
|
});
|
|
235
244
|
server.registerTool("ler_artefato", {
|
|
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
|
-
|
|
245
|
+
description: `Lê a política ou o briefing do projeto direto do serviço.
|
|
246
|
+
|
|
247
|
+
- \`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.
|
|
248
|
+
- \`briefing\`: o retrato estável do projeto — stack, objetivo, restrições.
|
|
249
|
+
|
|
241
250
|
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.`,
|
|
242
251
|
inputSchema: z.object({
|
|
243
252
|
tipo: z
|
|
@@ -257,10 +266,10 @@ Devolve vazio quando o artefato ainda não existe. Vazio não é erro: projeto n
|
|
|
257
266
|
}
|
|
258
267
|
});
|
|
259
268
|
server.registerTool("escrever_artefato", {
|
|
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
|
-
|
|
269
|
+
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.
|
|
270
|
+
|
|
271
|
+
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.
|
|
272
|
+
|
|
264
273
|
Conteúdo vazio é válido e significa apagar o artefato.`,
|
|
265
274
|
inputSchema: z.object({
|
|
266
275
|
tipo: z
|
package/package.json
CHANGED