dd-harness 0.8.0 → 0.10.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,50 @@
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
+ /** Uma palavra suspeita encontrada no texto. */
29
+ export type Suspeita = {
30
+ escrito: string;
31
+ correto: string;
32
+ };
33
+ /**
34
+ * Devolve as palavras que exigem acento e apareceram sem ele.
35
+ *
36
+ * Percorre o texto ja em minusculas e sem os trechos onde ASCII e legitimo: bloco de
37
+ * codigo (``` ... ```), codigo inline (`...`), URL e caminho de arquivo. Sem isso, um
38
+ * `preco_centavos` no meio de um identificador acusaria prosa que esta correta.
39
+ */
40
+ export declare function palavrasSemAcento(texto: string): Suspeita[];
41
+ /**
42
+ * A mensagem de recusa, ou `null` se o texto passa.
43
+ *
44
+ * Recebe os campos ja separados para poder dizer ONDE esta o problema — quem escreveu o
45
+ * corpo inteiro sem acento nao quer procurar a palavra em 40 linhas.
46
+ */
47
+ export declare function recusaPorAcentuacao(campos: {
48
+ campo: string;
49
+ texto: string;
50
+ }[]): string | null;
@@ -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/config.d.ts CHANGED
@@ -17,6 +17,17 @@ export type ConfigDoRepo = {
17
17
  export declare const CAMINHO_CONFIG = ".dd-harness.json";
18
18
  export declare const CAMINHO_MANIFESTO = "dd-harness/manifest.json";
19
19
  export declare function leConfigDoRepo(raiz: string): Promise<ConfigDoRepo>;
20
+ export type ConfigDaMaquina = {
21
+ /**
22
+ * Onde o monorepo `dd-harness_new` esta clonado nesta maquina. O worker de embeddings
23
+ * (`pnpm worker`) so existe ali dentro — sem este caminho, o hook de sessao nao tem
24
+ * como subir o worker quando roda de dentro do projeto CONSUMIDOR, que e o caso comum.
25
+ * Opcional: sem ele, so degrada para "avisa que a fila esta parada", nunca quebra.
26
+ */
27
+ workerRepo?: string;
28
+ };
29
+ export declare function leConfigDaMaquina(): Promise<ConfigDaMaquina>;
30
+ export declare function guardaConfigDaMaquina(parcial: ConfigDaMaquina): Promise<string>;
20
31
  /**
21
32
  * Credencial por origem da API: a mesma maquina pode falar com uma instalacao local e
22
33
  * com a de producao, e misturar token entre as duas daria 401 confuso.
package/dist/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,41 @@
1
+ /**
2
+ * Escrita de `.claude/settings.json`, `.mcp.json` e `AGENTS.md`, com merge.
3
+ *
4
+ * `init` (o comando antigo) so SUGERE esses arquivos — decisao de proposito, porque
5
+ * mesclar JSON alheio e onde falha silenciosa nasce. `start` (o wizard) reverte essa
6
+ * decisao para ganhar "funciona de primeira sem copiar e colar", mas o preco combinado
7
+ * e nunca perder o que a pessoa ja tinha: cada funcao aqui LE o arquivo antes de
8
+ * escrever, e so INSERE a chave do dd-harness — nunca substitui o resto.
9
+ *
10
+ * JSON que nao interpreta e tratado como "nao sei mesclar", nao como "esta vazio": e
11
+ * mais seguro parar e avisar do que sobrescrever um arquivo que a pessoa escreveu a mao
12
+ * e que, por acaso, tem um erro de sintaxe.
13
+ */
14
+ export type ResultadoDaEscrita<T extends string> = {
15
+ ok: true;
16
+ estado: T;
17
+ } | {
18
+ ok: false;
19
+ motivo: "json-invalido";
20
+ caminho: string;
21
+ };
22
+ /**
23
+ * Acrescenta o hook do dd-harness a `.claude/settings.json`, sem tocar em outros hooks.
24
+ *
25
+ * `SessionStart` e um ARRAY de matcher groups no schema do Claude Code — pensado para
26
+ * varios hooks convivendo. Por isso a acao certa quando ja existe outro hook de
27
+ * SessionStart e ACRESCENTAR um grupo novo ao array, nao substituir nem perguntar: sao
28
+ * dois hooks independentes, cada um cuidando da sua coisa.
29
+ */
30
+ export declare function escreveHook(raiz: string): Promise<ResultadoDaEscrita<"criado" | "ja-tinha" | "acrescentado">>;
31
+ /** Acrescenta o servidor MCP a `.mcp.json`, sem tocar em outros servidores. */
32
+ export declare function escreveMcp(raiz: string): Promise<ResultadoDaEscrita<"criado" | "ja-tinha" | "acrescentado">>;
33
+ /**
34
+ * `AGENTS.md` e markdown livre, nao JSON — nao ha "mesclar" de verdade. Se o arquivo ja
35
+ * existe (com qualquer conteudo), so acrescenta a instrucao no fim; nunca sobrescreve
36
+ * o que a pessoa escreveu, porque markdown de outra ferramenta e mais dificil de separar
37
+ * do nosso do que uma chave de objeto.
38
+ */
39
+ export declare function escreveAgents(raiz: string): Promise<{
40
+ estado: "criado" | "ja-tinha" | "acrescentado";
41
+ }>;
@@ -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/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/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 { leConfigDoRepo, guardaToken } from "./config.js";
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,166 @@ 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
+ * Forca o `npx` a baixar e cachear `dd-harness-mcp` AGORA, dentro do wizard — nunca na
84
+ * primeira conexao do Claude Code. Sem isto, a primeira instalacao acontecia so quando
85
+ * o host tentava conectar ao servidor MCP, e o handshake tem timeout curto demais para
86
+ * esperar o download: a sessao real via `CONNECTION_CLOSED` em vez de "instalando".
87
+ */
88
+ function instalaMcp() {
89
+ return new Promise((resolve) => {
90
+ const p = spawn("npx", ["-y", "dd-harness-mcp", "--help"], {
91
+ shell: process.platform === "win32",
92
+ stdio: "ignore",
93
+ });
94
+ p.on("exit", (code) => resolve(code === 0));
95
+ p.on("error", () => resolve(false));
96
+ });
97
+ }
98
+ /**
99
+ * Tenta abrir o navegador padrao. Nunca lanca — falhar em abrir nao pode travar o
100
+ * wizard, so degrada para "aqui esta a URL, abra voce mesmo".
101
+ */
102
+ function tentaAbrirNavegador(url) {
103
+ const comando = process.platform === "win32" ? "start" : process.platform === "darwin" ? "open" : "xdg-open";
104
+ try {
105
+ spawn(comando, process.platform === "win32" ? ["", url] : [url], {
106
+ shell: process.platform === "win32",
107
+ stdio: "ignore",
108
+ detached: true,
109
+ }).unref();
110
+ }
111
+ catch {
112
+ // Segue sem navegador — a URL impressa antes desta chamada ja resolve.
113
+ }
114
+ }
115
+ /**
116
+ * `dd-harness start` — o wizard para pasta vazia.
117
+ *
118
+ * Ate aqui, comecar um projeto exigia saber de memoria a sequencia certa
119
+ * (`projeto` -> `init` -> copiar tres blocos de JSON) e o slug do tenant. `start`
120
+ * conduz tudo numa tacada, perguntando so o que falta.
121
+ *
122
+ * Reverte "sugerir, nunca escrever" para `.claude/settings.json` e `.mcp.json` — decisao
123
+ * tomada de proposito aqui, ao contrario de `init`, que continua so sugerindo. O preco e
124
+ * o merge cuidadoso em `escreve-config.ts`: nunca perder o que a pessoa ja tinha.
125
+ */
126
+ async function comandoStart() {
127
+ const caminhoConfig = join(process.cwd(), CAMINHO_CONFIG);
128
+ try {
129
+ await access(caminhoConfig);
130
+ throw new Error(`este repositório já tem ${CAMINHO_CONFIG} — start é só para pasta vazia. ` +
131
+ "Use `dd-harness init` para ajustar um projeto já existente.");
132
+ }
133
+ catch (erro) {
134
+ if (erro instanceof Error && erro.message.includes("já tem"))
135
+ throw erro;
136
+ // ENOENT: exatamente o caso esperado, segue.
137
+ }
138
+ console.log("Iniciando o dd-harness nesta pasta.\n");
139
+ // 1. Credencial. Sem ela nada do resto e possivel — nem listar tenant.
140
+ let token = await leToken(API_PADRAO);
141
+ if (!token) {
142
+ console.log(`Sem credencial salva para ${API_PADRAO}.`);
143
+ token = await pergunta("Cole o token pessoal (crie um em /tokens na interface): ");
144
+ await guardaToken(API_PADRAO, token);
145
+ console.log("Credencial guardada.\n");
146
+ }
147
+ // 2. Espaco (tenant). Criar um e ato de dono — nunca por aqui — mas ESCOLHER entre
148
+ // os que ja existem e so leitura, e e o que faltava para nao exigir o slug de memoria.
149
+ let tenants = await listaTenants(API_PADRAO, token);
150
+ while (tenants.length === 0) {
151
+ const url = `${API_PADRAO}/tenants`;
152
+ console.log(`\nVocê ainda não participa de nenhum espaço. Abrindo ${url} ...`);
153
+ tentaAbrirNavegador(url);
154
+ await pergunta("Crie um espaço lá e volte aqui. Pressione Enter para continuar: ");
155
+ tenants = await listaTenants(API_PADRAO, token);
156
+ }
157
+ const tenant = await escolha("\nEscolha um espaço:", tenants.map((t) => ({ rotulo: `${t.nome} (${t.slug})`, valor: t.slug })));
158
+ // 3. Projeto. Criar e subordinado a estar num espaco — isso o CLI ja faz sozinho.
159
+ const nomeSugerido = process.cwd().split(/[\\/]/).pop() ?? "meu-projeto";
160
+ const nome = (await pergunta(`\nNome do projeto [${nomeSugerido}]: `)) || nomeSugerido;
161
+ // Nome so com caracteres especiais (ex: "!!!") normaliza para string vazia — o
162
+ // fallback evita sugerir slug vazio, que o servidor recusaria com erro confuso.
163
+ const slugSugerido = nome
164
+ .toLowerCase()
165
+ .normalize("NFD")
166
+ .replace(/[̀-ͯ]/g, "")
167
+ .replace(/[^a-z0-9]+/g, "-")
168
+ .replace(/^-+|-+$/g, "") || "meu-projeto";
169
+ const slugDoProjeto = (await pergunta(`Identificador curto (slug) [${slugSugerido}]: `)) || slugSugerido;
170
+ const resultadoProjeto = await criaProjeto(process.cwd(), slugDoProjeto, nome, {
171
+ tenant,
172
+ api: API_PADRAO,
173
+ });
174
+ console.log(resultadoProjeto.jaExistia
175
+ ? `\nProjeto ${resultadoProjeto.projeto} já existia — usando ele.`
176
+ : `\nProjeto ${resultadoProjeto.projeto} criado.`);
177
+ // 4. Config do repositorio — o unico arquivo que `init` tambem escreveria.
178
+ const r = await init(process.cwd(), { tenant, projeto: resultadoProjeto.projeto, api: API_PADRAO });
179
+ console.log(r.config === "criada" ? `criado ${CAMINHO_CONFIG}` : `mantido ${CAMINHO_CONFIG}`);
180
+ // 5. Hook e MCP — ESCRITOS, com merge. E aqui que `start` diverge de `init`.
181
+ const hook = await escreveHook(process.cwd());
182
+ if (!hook.ok) {
183
+ console.log(`\nAVISO: ${hook.caminho} não é um JSON válido — não toquei nele. ` +
184
+ "Acrescente manualmente:\n");
185
+ console.log(SUGESTAO_HOOK);
186
+ }
187
+ else {
188
+ console.log({ criado: "criado .claude/settings.json (hook adicionado)",
189
+ "ja-tinha": "mantido .claude/settings.json — hook já estava lá",
190
+ acrescentado: "atualizado .claude/settings.json (hook adicionado ao que já existia)",
191
+ }[hook.estado]);
192
+ }
193
+ const mcp = await escreveMcp(process.cwd());
194
+ if (!mcp.ok) {
195
+ console.log(`\nAVISO: ${mcp.caminho} não é um JSON válido — não toquei nele. ` +
196
+ "Acrescente manualmente:\n");
197
+ console.log(SUGESTAO_MCP);
198
+ }
199
+ else {
200
+ console.log({ criado: "criado .mcp.json (servidor dd-harness adicionado)",
201
+ "ja-tinha": "mantido .mcp.json — servidor já estava declarado",
202
+ acrescentado: "atualizado .mcp.json (servidor adicionado ao que já existia)",
203
+ }[mcp.estado]);
204
+ }
205
+ if (mcp.ok) {
206
+ console.log("\nInstalando o servidor dd-harness-mcp (primeira vez pode demorar um pouco)...");
207
+ const instalou = await instalaMcp();
208
+ console.log(instalou
209
+ ? "dd-harness-mcp instalado — a próxima sessão do Claude Code conecta na hora."
210
+ : "AVISO: não consegui pré-instalar dd-harness-mcp. A primeira conexão do Claude " +
211
+ "Code pode demorar ou falhar; se falhar, abra uma nova sessão e tente de novo.");
212
+ }
213
+ // 6. AGENTS.md — so quem usa outra ferramenta alem do Claude Code precisa.
214
+ const outraFerramenta = await pergunta("\nVai abrir este projeto em Codex, Cursor ou outra ferramenta além do Claude " +
215
+ "Code? [s/N]: ");
216
+ if (/^s/i.test(outraFerramenta)) {
217
+ const agents = await escreveAgents(process.cwd());
218
+ console.log({ criado: "criado AGENTS.md",
219
+ "ja-tinha": "mantido AGENTS.md — já apontava para a política",
220
+ acrescentado: "atualizado AGENTS.md (instrução acrescentada ao que já existia)",
221
+ }[agents.estado]);
222
+ }
223
+ // 7. Onde esta o monorepo do worker nesta maquina? So pergunta uma vez, e so importa
224
+ // se houver memoria na fila agora — pular aqui nao trava nada, so avisa mais vezes.
225
+ const semWorkerConfigurado = !(await temWorkerConfigurado());
226
+ if (semWorkerConfigurado) {
227
+ const caminho = await pergunta("\nO worker de embeddings roda local (não hospedado). Onde está o repositório " +
228
+ "`dd-harness_new` clonado nesta máquina? (Enter para pular — o hook vai " +
229
+ "avisar quando precisar): ");
230
+ if (caminho)
231
+ await guardaConfigDaMaquina({ workerRepo: caminho });
232
+ }
233
+ fechaPerguntas();
234
+ console.log("\nPronto. Abra uma sessão do Claude Code nesta pasta — a política vai ser o " +
235
+ "primeiro assunto.");
236
+ }
237
+ async function temWorkerConfigurado() {
238
+ const { workerRepo } = await leConfigDaMaquina();
239
+ return Boolean(workerRepo);
240
+ }
74
241
  async function comandoInit(argv) {
75
242
  const tenant = argumento(argv, "tenant");
76
243
  const projeto = argumento(argv, "projeto");
@@ -527,6 +694,8 @@ async function principal() {
527
694
  return;
528
695
  }
529
696
  switch (comando) {
697
+ case "start":
698
+ return comandoStart();
530
699
  case "init":
531
700
  return comandoInit(resto);
532
701
  case "login":
@@ -0,0 +1,7 @@
1
+ export declare function pergunta(texto: string): Promise<string>;
2
+ export declare function fechaPerguntas(): void;
3
+ /** 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. */
4
+ export declare function escolha<T>(titulo: string, itens: {
5
+ rotulo: string;
6
+ valor: T;
7
+ }[]): Promise<T>;
@@ -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/projeto.d.ts CHANGED
@@ -17,3 +17,11 @@ export declare function criaProjeto(raiz: string, slug: string, nome: string, ex
17
17
  projeto: string;
18
18
  jaExistia: boolean;
19
19
  }>;
20
+ /**
21
+ * Os espacos (tenants) que este token alcanca — para `dd-harness start` deixar escolher
22
+ * em vez de exigir o slug de memoria.
23
+ */
24
+ export declare function listaTenants(api: string, token: string): Promise<{
25
+ slug: string;
26
+ nome: string;
27
+ }[]>;
package/dist/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/worker.d.ts CHANGED
@@ -1,21 +1,12 @@
1
1
  /**
2
- * Subir o worker de embeddings quando ha fila esperando.
2
+ * Onde rodar o worker, se soubermos.
3
3
  *
4
- * O worker nao esta hospedado (decisao de custo, registrada no ROADMAP): roda local, a
5
- * mao. O problema disso e a degradacao SILENCIOSA a memoria e gravada, a busca responde
6
- * `semantica: true` porque a CONSULTA foi vetorizada, e mesmo assim nao acha nada, porque
7
- * a BASE ainda nao tem vetor. Quem procura conclui "nao existe" quando o certo era
8
- * "ainda nao indexei".
9
- *
10
- * Por isso o hook de sessao sobe o worker sozinho. Duas travas, e as duas importam:
11
- *
12
- * - **So sobe quando ha fila.** O hook roda em TODA sessao, inclusive nas que so leem
13
- * codigo. Subir com a fila vazia gastaria chamada de embedding sem ninguem ter pedido.
14
- * - **So no repositorio que TEM o worker.** Ele vive neste monorepo, nao no repositorio
15
- * consumidor: la o `pnpm worker` nao existe, e tentar rodar daria erro a cada sessao.
4
+ * Prioridade: `~/.dd-harness/config.json` (`workerRepo`, salvo por `dd-harness start` ou
5
+ * `dd-harness worker --configurar`) primeiro e o caminho que vale em qualquer pasta que
6
+ * o CLI rode. Cai para `raiz` so por compatibilidade com quem roda de dentro do
7
+ * monorepo sem ter configurado nada.
16
8
  */
17
- /** O worker vive aqui dentro. Noutro repositorio, nao ha o que subir. */
18
- export declare function temWorkerLocal(raiz: string): Promise<boolean>;
9
+ export declare function achaOWorker(raiz: string): Promise<string | null>;
19
10
  export type Subida = {
20
11
  subiu: true;
21
12
  } | {
package/dist/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 no repositorio que TEM o worker.** Ele vive neste monorepo, nao no repositorio
18
- * consumidor: la o `pnpm worker` nao existe, e tentar rodar daria erro a cada sessao.
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 vive aqui dentro. Noutro repositorio, nao ha o que subir. */
21
- export async function temWorkerLocal(raiz) {
24
+ /** O worker existe nesta pasta especifica? */
25
+ async function ehOMonorepo(caminho) {
22
26
  try {
23
- await access(join(raiz, "src", "worker", "indexador.ts"));
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
- if (!(await temWorkerLocal(raiz))) {
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: raiz,
69
+ cwd: repo,
50
70
  detached: true,
51
71
  stdio: "ignore",
52
72
  shell: process.platform === "win32",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-harness",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "description": "Cliente do dd-harness: politica no inicio da sessao, e memoria por busca — nada em disco. Sem dependencia: fetch, crypto e fs sao do Node.",
6
6
  "license": "UNLICENSED",