dd-harness 0.30.0 → 0.32.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/README.md +103 -64
- package/dist/api.d.ts +2 -0
- package/dist/api.js +29 -25
- package/dist/artefato.js +6 -2
- package/dist/cinto.d.ts +2 -0
- package/dist/cinto.js +29 -10
- package/dist/config.d.ts +4 -0
- package/dist/config.js +35 -6
- package/dist/curar.js +9 -4
- package/dist/diagnostico.d.ts +5 -0
- package/dist/diagnostico.js +92 -0
- package/dist/edicoes.d.ts +15 -0
- package/dist/edicoes.js +52 -0
- package/dist/escreve-config.d.ts +1 -0
- package/dist/escreve-config.js +9 -3
- package/dist/estado-local.d.ts +7 -0
- package/dist/estado-local.js +33 -0
- package/dist/gravar.js +5 -15
- package/dist/hosts.d.ts +6 -0
- package/dist/hosts.js +129 -0
- package/dist/index.js +69 -96
- package/dist/pasta.js +2 -2
- package/dist/politica.js +4 -2
- package/dist/projeto.js +2 -2
- package/dist/regras-de-commit.d.ts +3 -0
- package/dist/regras-de-commit.js +15 -0
- package/dist/roadmap.js +3 -3
- package/dist/sessao.d.ts +5 -0
- package/dist/sessao.js +125 -0
- package/dist/skill.d.ts +7 -9
- package/dist/skill.js +101 -30
- package/dist/versao.d.ts +1 -0
- package/dist/versao.js +18 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +21 -10
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,64 +1,103 @@
|
|
|
1
|
-
# dd-harness
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
```sh
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
dd-harness
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
1
|
+
# dd-harness
|
|
2
|
+
|
|
3
|
+
Política e briefing carregados do serviço; memória consultada por MCP ou CLI.
|
|
4
|
+
A fonte dos artefatos continua no serviço. O cliente mantém configuração, ponteiros
|
|
5
|
+
para skills e estado descartável fora do repositório.
|
|
6
|
+
|
|
7
|
+
Requer Node >=20.3.0. CLI sem dependências de runtime.
|
|
8
|
+
|
|
9
|
+
## Instalação e configuração
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install -g dd-harness@latest
|
|
13
|
+
npx -y dd-harness-mcp@latest --help
|
|
14
|
+
dd-harness start --host todos
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Para um projeto já criado no serviço:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
dd-harness init --tenant meu-espaco --projeto meu-projeto
|
|
21
|
+
dd-harness login --token <token>
|
|
22
|
+
dd-harness integrar --host claude,codex,antigravity
|
|
23
|
+
dd-harness diagnostico --mcp
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`start` usa Claude por padrão. `--host` aceita `claude`, `codex`,
|
|
27
|
+
`antigravity`, uma lista separada por vírgulas ou `todos`.
|
|
28
|
+
`integrar` instala nos três por padrão e preserva configurações alheias.
|
|
29
|
+
O token nasce em /tokens e fica em ~/.dd-harness/credentials.json, por origem de API.
|
|
30
|
+
Para serviço local, use `init --api http://localhost:3000` antes do login.
|
|
31
|
+
|
|
32
|
+
## Hosts
|
|
33
|
+
|
|
34
|
+
| Host | MCP | Boot e guarda | Skills |
|
|
35
|
+
|---|---|---|---|
|
|
36
|
+
| Claude Code | .mcp.json | .claude/settings.json | .claude/skills |
|
|
37
|
+
| Codex | .codex/config.toml | .codex/hooks.json | .agents/skills |
|
|
38
|
+
| Antigravity | .agents/mcp_config.json | .agents/hooks.json | .agents/skills |
|
|
39
|
+
|
|
40
|
+
Codex exige revisão/confiança dos hooks em /hooks. Instalação não comprova ativação.
|
|
41
|
+
Antigravity usa PreInvocation e mensagem efêmera: política e briefing são consultados
|
|
42
|
+
antes de cada inferência. Sua guarda usa `ask` para respeitar concessões já existentes;
|
|
43
|
+
ela pode acrescentar confirmações. Não amplia permissões automaticamente.
|
|
44
|
+
|
|
45
|
+
Sem boot válido, a guarda recusa ferramentas cobertas, inclusive shell. Leitura conhecida
|
|
46
|
+
e onboarding controlado continuam possíveis. Os hooks precisam estar ativos: não são
|
|
47
|
+
sandbox nem controlam ferramentas que o aplicativo não encaminha a eles.
|
|
48
|
+
Após boot válido, shell não tem análise de diff antecipada; avisos por âncora cobrem
|
|
49
|
+
edições estruturadas, patches, remoções e renomeações.
|
|
50
|
+
|
|
51
|
+
Configurações MCP novas levam DD_HARNESS_ROOT explícito. Ao copiar/mover um checkout,
|
|
52
|
+
confira essa raiz com diagnostico. O MCP recusa divergência entre a raiz explícita e
|
|
53
|
+
o projeto identificado no diretório de execução. Corrija a configuração e reinicie o host.
|
|
54
|
+
|
|
55
|
+
## Uso diário
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
dd-harness politica
|
|
59
|
+
dd-harness buscar "contrato de integração"
|
|
60
|
+
dd-harness ler regras/contrato
|
|
61
|
+
dd-harness gravar memoria.md
|
|
62
|
+
dd-harness editar memoria.md
|
|
63
|
+
dd-harness arquivar regras/contrato --motivo obsoleta
|
|
64
|
+
dd-harness status
|
|
65
|
+
dd-harness skills
|
|
66
|
+
dd-harness roadmap
|
|
67
|
+
dd-harness changelog
|
|
68
|
+
dd-harness --help
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`check` mede e grava observações de deriva; `status` só consulta.
|
|
72
|
+
O antigo comando `sync` não faz parte do CLI atual.
|
|
73
|
+
`politica --hook` e `cinto` permanecem como compatibilidade legada;
|
|
74
|
+
use `integrar` para instalar `sessao` e `guarda`.
|
|
75
|
+
|
|
76
|
+
## Estado e conflitos
|
|
77
|
+
|
|
78
|
+
- Política/briefing não são materializados. Âncoras/resumos, sessão e manifestos ficam
|
|
79
|
+
em ~/.dd-harness/repos, isolados por caminho/projeto/credencial quando aplicável.
|
|
80
|
+
- DD_HARNESS_HOME permite isolamento explícito em testes. Nunca aponte testes ao estado pessoal.
|
|
81
|
+
- Uma sessão validada expira em quatro horas. Boot, retomada e compactação revalidam.
|
|
82
|
+
Alterações locais de política/briefing via MCP invalidam a sessão; reabra depois de editar.
|
|
83
|
+
Alterações remotas feitas por outro cliente são percebidas no próximo boot/revalidação.
|
|
84
|
+
- Curadoria invalida as âncoras do checkout atual. A próxima edição as consulta novamente.
|
|
85
|
+
- Só ponteiros registrados e intactos são atualizados/removidos. Skills manuais, editadas
|
|
86
|
+
ou redirecionadas por links são preservadas e aparecem como conflitos.
|
|
87
|
+
- Skills marcadas só por comando não são instaladas para descoberta automática em
|
|
88
|
+
.agents; use listar_skills/ler_skill após pedido explícito. Metadados próprios do
|
|
89
|
+
Claude não são prometidos como portáveis.
|
|
90
|
+
- Se há fila e worker local configurado, o boot solicita um lote. O log fica em
|
|
91
|
+
~/.dd-harness/worker.log; solicitar não significa que a indexação concluiu.
|
|
92
|
+
|
|
93
|
+
## Escritas e commits
|
|
94
|
+
|
|
95
|
+
Só GET/HEAD têm retry automático (até três tentativas, timeout por tentativa).
|
|
96
|
+
POST/PATCH/PUT/DELETE não são repetidos. Falha de conexão, 5xx ou resposta incompleta
|
|
97
|
+
em escrita exige consultar o estado antes de tentar novamente.
|
|
98
|
+
|
|
99
|
+
A política entregue exige `[IDENTIFICAÇÃO] - tipo: descrição` nos commits de agentes:
|
|
100
|
+
`[CODEX] - feat: ...`, `[CLAUDE] - fix: ...`, `[GEMINI] - docs: ...`.
|
|
101
|
+
Identifique quem realmente commitou, inclusive ao usar outro editor. A regra não altera
|
|
102
|
+
Git author nem concede autorização de commit/push. É regra de trabalho do agente,
|
|
103
|
+
não inferência automática de identidade nem hook que renomeia commits humanos.
|
package/dist/api.d.ts
CHANGED
|
@@ -19,3 +19,5 @@ export declare function credencial(raiz: string): Promise<Credencial>;
|
|
|
19
19
|
export declare function recusa(resposta: Response): Promise<never>;
|
|
20
20
|
export declare function cabecalhos(token: string, comCorpo?: boolean): Record<string, string>;
|
|
21
21
|
export declare function pede(url: string | URL, init?: RequestInit, tentativas?: number): Promise<Response>;
|
|
22
|
+
/** A conexão também pode cair depois dos headers, enquanto o corpo é consumido. */
|
|
23
|
+
export declare function jsonDaEscrita(resposta: Response): Promise<unknown>;
|
package/dist/api.js
CHANGED
|
@@ -20,44 +20,39 @@ export function cabecalhos(token, comCorpo = false) {
|
|
|
20
20
|
? { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }
|
|
21
21
|
: { Authorization: `Bearer ${token}` };
|
|
22
22
|
}
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
* funcionou na segunda tentativa manual. Sem retry, falha de rede de um segundo vira erro
|
|
28
|
-
* final — e para um agente isso e a diferenca entre seguir sozinho e parar para pedir
|
|
29
|
-
* ajuda. Com varios agentes concorrendo, transitorio deixa de ser raro.
|
|
30
|
-
*
|
|
31
|
-
* O que NAO e repetido, e o cuidado que importa:
|
|
32
|
-
*
|
|
33
|
-
* - Erro de aplicacao (4xx). Token invalido ou memoria fora dos filtros nao melhora na
|
|
34
|
-
* segunda tentativa; repetir so atrasa a mensagem que o agente precisa ler.
|
|
35
|
-
* - 5xx em requisicao que ESCREVE. `POST /memorias` pode ter gravado antes de a resposta
|
|
36
|
-
* se perder, e repetir criaria duas. Perder a resposta de uma escrita que funcionou e
|
|
37
|
-
* ruim; gravar duas vezes e pior.
|
|
38
|
-
*
|
|
39
|
-
* Entao: repete falha de REDE (o `fetch` nem chegou a receber resposta) sempre, e 5xx
|
|
40
|
-
* apenas quando o metodo e seguro de repetir.
|
|
41
|
-
*/
|
|
42
|
-
const IDEMPOTENTES = new Set(["GET", "HEAD", "PUT", "DELETE"]);
|
|
23
|
+
/** Retry limitado a leituras; erro de transporte em escrita deixa resultado incerto. */
|
|
24
|
+
// Mesmo PUT/DELETE podem ter auditoria, triggers ou conflito após a primeira aplicação.
|
|
25
|
+
// Só leituras são repetidas automaticamente; erro de transporte não prova ausência de commit.
|
|
26
|
+
const LEITURAS = new Set(["GET", "HEAD"]);
|
|
43
27
|
const espera = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
44
28
|
export async function pede(url, init = {}, tentativas = 3) {
|
|
45
29
|
const metodo = (init.method ?? "GET").toUpperCase();
|
|
46
|
-
const podeRepetirErroDoServidor =
|
|
30
|
+
const podeRepetirErroDoServidor = LEITURAS.has(metodo);
|
|
31
|
+
if (!Number.isInteger(tentativas) || tentativas < 1)
|
|
32
|
+
throw new Error("tentativas deve ser inteiro positivo");
|
|
47
33
|
let ultimoErro;
|
|
48
34
|
for (let tentativa = 1; tentativa <= tentativas; tentativa += 1) {
|
|
49
35
|
try {
|
|
50
|
-
const
|
|
36
|
+
const timeout = AbortSignal.timeout(10000);
|
|
37
|
+
const signal = init.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
|
|
38
|
+
const resposta = await fetch(url, { ...init, signal });
|
|
39
|
+
if (resposta.status >= 500 && !podeRepetirErroDoServidor) {
|
|
40
|
+
await resposta.body?.cancel();
|
|
41
|
+
throw new Error(`HTTP ${resposta.status}: o servidor pode ter aplicado a escrita antes de falhar`);
|
|
42
|
+
}
|
|
51
43
|
if (resposta.status >= 500 && podeRepetirErroDoServidor && tentativa < tentativas) {
|
|
44
|
+
await resposta.body?.cancel();
|
|
52
45
|
await espera(tentativa * 400);
|
|
53
46
|
continue;
|
|
54
47
|
}
|
|
55
48
|
return resposta;
|
|
56
49
|
}
|
|
57
50
|
catch (erro) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
51
|
+
if (!podeRepetirErroDoServidor) {
|
|
52
|
+
throw new Error(`resultado incerto de ${metodo}: não houve confirmação confiável e a escrita pode ter sido aplicada. Consulte o estado antes de tentar novamente.`, { cause: erro });
|
|
53
|
+
}
|
|
54
|
+
if (init.signal?.aborted)
|
|
55
|
+
throw erro;
|
|
61
56
|
ultimoErro = erro;
|
|
62
57
|
if (tentativa === tentativas)
|
|
63
58
|
break;
|
|
@@ -67,3 +62,12 @@ export async function pede(url, init = {}, tentativas = 3) {
|
|
|
67
62
|
const detalhe = ultimoErro instanceof Error ? ultimoErro.message : String(ultimoErro);
|
|
68
63
|
throw new Error(`não consegui falar com o serviço depois de ${tentativas} tentativas: ${detalhe}`);
|
|
69
64
|
}
|
|
65
|
+
/** A conexão também pode cair depois dos headers, enquanto o corpo é consumido. */
|
|
66
|
+
export async function jsonDaEscrita(resposta) {
|
|
67
|
+
try {
|
|
68
|
+
return await resposta.json();
|
|
69
|
+
}
|
|
70
|
+
catch (cause) {
|
|
71
|
+
throw new Error("resultado incerto: a resposta da escrita ficou incompleta. Consulte o estado antes de tentar novamente.", { cause });
|
|
72
|
+
}
|
|
73
|
+
}
|
package/dist/artefato.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { cabecalhos, credencial, pede, recusa } from "./api.js";
|
|
1
|
+
import { jsonDaEscrita, cabecalhos, credencial, pede, recusa } from "./api.js";
|
|
2
|
+
import { invalidaContexto } from "./estado-local.js";
|
|
2
3
|
/**
|
|
3
4
|
* O `GET` devolve o payload inteiro (politica, briefing, pastas e memorias); aqui so o
|
|
4
5
|
* artefato pedido interessa.
|
|
@@ -38,5 +39,8 @@ export async function escreveArtefato(raiz, tipo, conteudo) {
|
|
|
38
39
|
});
|
|
39
40
|
if (!resposta.ok)
|
|
40
41
|
await recusa(resposta);
|
|
41
|
-
|
|
42
|
+
await invalidaContexto(raiz).catch(() => {
|
|
43
|
+
throw new Error("Artefato aceito pela API, mas a sessão local não pôde ser invalidada. Reabra a sessão antes de trabalhar.");
|
|
44
|
+
});
|
|
45
|
+
return (await jsonDaEscrita(resposta));
|
|
42
46
|
}
|
package/dist/cinto.d.ts
CHANGED
|
@@ -21,12 +21,14 @@ import type { Brain, Memoria } from "./brain.js";
|
|
|
21
21
|
*/
|
|
22
22
|
/** Onde o cache vive. Fora do repositorio: nada do dd-harness volta a morar em disco versionado. */
|
|
23
23
|
export declare function caminhoDoCache(raiz: string): string;
|
|
24
|
+
export declare function invalidaCache(raiz: string): Promise<void>;
|
|
24
25
|
/**
|
|
25
26
|
* So o que o cruzamento precisa. O payload inteiro tem briefing, politica e o corpo de
|
|
26
27
|
* toda memoria — carregar isso a cada `Edit` seria pagar leitura de disco grande para,
|
|
27
28
|
* quase sempre, nao casar com nada.
|
|
28
29
|
*/
|
|
29
30
|
export type CacheDeAncoras = {
|
|
31
|
+
projeto?: string;
|
|
30
32
|
gravado_em: string;
|
|
31
33
|
memorias: Pick<Memoria, "pasta" | "slug" | "titulo" | "resumo" | "status" | "ancoras" | "escopo">[];
|
|
32
34
|
};
|
package/dist/cinto.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readFile, writeFile, rm } from "node:fs/promises";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { estadoDoRepo, escreveAtomico } from "./estado-local.js";
|
|
4
|
+
import { leConfigDoRepo, leToken } from "./config.js";
|
|
2
5
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
3
6
|
import { memoriasNoConteudo } from "./diff.js";
|
|
4
7
|
/**
|
|
@@ -23,7 +26,16 @@ import { memoriasNoConteudo } from "./diff.js";
|
|
|
23
26
|
*/
|
|
24
27
|
/** Onde o cache vive. Fora do repositorio: nada do dd-harness volta a morar em disco versionado. */
|
|
25
28
|
export function caminhoDoCache(raiz) {
|
|
26
|
-
return join(raiz, "
|
|
29
|
+
return join(estadoDoRepo(raiz), "ancoras.json");
|
|
30
|
+
}
|
|
31
|
+
async function identidade(raiz) {
|
|
32
|
+
const config = await leConfigDoRepo(raiz).catch(() => null);
|
|
33
|
+
return createHash("sha256").update(JSON.stringify([config, config ? await leToken(config.api) : null])).digest("hex");
|
|
34
|
+
}
|
|
35
|
+
export async function invalidaCache(raiz) {
|
|
36
|
+
await rm(caminhoDoCache(raiz), { force: true }).catch(() => {
|
|
37
|
+
throw new Error("Gravação aceita pela API, mas o cache local não pôde ser invalidado. Reabra a sessão para reconstruí-lo.");
|
|
38
|
+
});
|
|
27
39
|
}
|
|
28
40
|
/**
|
|
29
41
|
* Acrescenta UMA memoria ao cache, sem esperar a proxima sessao.
|
|
@@ -40,21 +52,23 @@ export function caminhoDoCache(raiz) {
|
|
|
40
52
|
* Idempotente pelo endereco: regravar a mesma memoria substitui a entrada, nunca duplica.
|
|
41
53
|
*/
|
|
42
54
|
export async function acrescentaAoCache(raiz, memoria) {
|
|
43
|
-
|
|
55
|
+
const existente = await leCache(raiz);
|
|
56
|
+
if (!existente && memoria.ancoras.length === 0)
|
|
44
57
|
return;
|
|
45
|
-
const atual =
|
|
58
|
+
const atual = existente ?? { gravado_em: "", memorias: [] };
|
|
46
59
|
const endereco = `${memoria.pasta}/${memoria.slug}`;
|
|
47
60
|
const cache = {
|
|
61
|
+
projeto: await identidade(raiz),
|
|
48
62
|
gravado_em: new Date().toISOString(),
|
|
49
63
|
memorias: [
|
|
50
64
|
...atual.memorias.filter((m) => `${m.pasta}/${m.slug}` !== endereco),
|
|
51
|
-
memoria,
|
|
65
|
+
...(memoria.status === "ativa" && memoria.ancoras.length ? [memoria] : []),
|
|
52
66
|
],
|
|
53
67
|
};
|
|
54
68
|
try {
|
|
55
69
|
const caminho = caminhoDoCache(raiz);
|
|
56
70
|
await mkdir(dirname(caminho), { recursive: true });
|
|
57
|
-
await
|
|
71
|
+
await escreveAtomico(caminho, `${JSON.stringify(cache)}\n`);
|
|
58
72
|
}
|
|
59
73
|
catch {
|
|
60
74
|
// Mesmo motivo do `guardaCache`: cache e otimizacao, nao contrato.
|
|
@@ -63,6 +77,7 @@ export async function acrescentaAoCache(raiz, memoria) {
|
|
|
63
77
|
/** Guarda as ancoras do Brain para o hook consultar sem rede. Falha em silencio: cache e otimizacao, nao contrato. */
|
|
64
78
|
export async function guardaCache(raiz, brain) {
|
|
65
79
|
const cache = {
|
|
80
|
+
projeto: await identidade(raiz),
|
|
66
81
|
gravado_em: new Date().toISOString(),
|
|
67
82
|
memorias: brain.memorias
|
|
68
83
|
.filter((m) => m.status === "ativa" && m.ancoras.length > 0)
|
|
@@ -79,7 +94,7 @@ export async function guardaCache(raiz, brain) {
|
|
|
79
94
|
try {
|
|
80
95
|
const caminho = caminhoDoCache(raiz);
|
|
81
96
|
await mkdir(dirname(caminho), { recursive: true });
|
|
82
|
-
await
|
|
97
|
+
await escreveAtomico(caminho, `${JSON.stringify(cache)}\n`);
|
|
83
98
|
}
|
|
84
99
|
catch {
|
|
85
100
|
// Sem cache o hook cala, e o PULL continua funcionando. Nao vale falhar a sessao.
|
|
@@ -87,7 +102,10 @@ export async function guardaCache(raiz, brain) {
|
|
|
87
102
|
}
|
|
88
103
|
export async function leCache(raiz) {
|
|
89
104
|
try {
|
|
90
|
-
|
|
105
|
+
const cache = JSON.parse(await readFile(caminhoDoCache(raiz), "utf8"));
|
|
106
|
+
if (cache.projeto !== await identidade(raiz) || !Array.isArray(cache.memorias))
|
|
107
|
+
return null;
|
|
108
|
+
return cache;
|
|
91
109
|
}
|
|
92
110
|
catch {
|
|
93
111
|
return null;
|
|
@@ -106,7 +124,8 @@ export async function leCache(raiz) {
|
|
|
106
124
|
* nao viu o aviso de hoje.
|
|
107
125
|
*/
|
|
108
126
|
async function jaAvisou(raiz, sessao, enderecos) {
|
|
109
|
-
const
|
|
127
|
+
const chave = createHash("sha256").update(sessao + await identidade(raiz)).digest("hex");
|
|
128
|
+
const caminho = join(estadoDoRepo(raiz), `avisos-${chave}.json`);
|
|
110
129
|
let vistos = [];
|
|
111
130
|
try {
|
|
112
131
|
vistos = JSON.parse(await readFile(caminho, "utf8"));
|
|
@@ -134,7 +153,7 @@ async function jaAvisou(raiz, sessao, enderecos) {
|
|
|
134
153
|
*/
|
|
135
154
|
export function caminhoRelativo(raiz, arquivo) {
|
|
136
155
|
const rel = relative(resolve(raiz), resolve(arquivo));
|
|
137
|
-
if (!rel || rel.startsWith(
|
|
156
|
+
if (!rel || rel === ".." || rel.startsWith(`..${sep}`) || /^[A-Za-z]:/.test(rel))
|
|
138
157
|
return null;
|
|
139
158
|
return rel.split(sep).join("/");
|
|
140
159
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -17,6 +17,10 @@ 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
|
+
/** Sobe apenas até a raiz git; não herda outro projeto acima de um worktree. */
|
|
21
|
+
export declare function achaRaiz(inicio?: string): Promise<string>;
|
|
22
|
+
/** Uma configuração copiada de outro checkout nunca pode direcionar operações silenciosamente. */
|
|
23
|
+
export declare function raizDoMcp(): Promise<string>;
|
|
20
24
|
export type ConfigDaMaquina = {
|
|
21
25
|
/**
|
|
22
26
|
* Onde o monorepo `dd-harness_new` esta clonado nesta maquina. O worker de embeddings
|
package/dist/config.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { diretorioDoUsuario } from "./estado-local.js";
|
|
5
5
|
/**
|
|
6
6
|
* O servico publicado e o padrao: um repositorio que so declara `tenant` e `projeto` ja
|
|
7
7
|
* fala com quem existe. Para desenvolver contra a maquina local, ponha `api` explicito no
|
|
@@ -30,8 +30,37 @@ export async function leConfigDoRepo(raiz) {
|
|
|
30
30
|
pasta: lido.pasta ?? PADRAO.pasta,
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
/** Sobe apenas até a raiz git; não herda outro projeto acima de um worktree. */
|
|
34
|
+
export async function achaRaiz(inicio = process.env.DD_HARNESS_ROOT ?? process.cwd()) {
|
|
35
|
+
let atual = resolve(inicio);
|
|
36
|
+
const { access } = await import("node:fs/promises");
|
|
37
|
+
for (;;) {
|
|
38
|
+
if (await access(join(atual, CAMINHO_CONFIG)).then(() => true, () => false))
|
|
39
|
+
return atual;
|
|
40
|
+
if (await access(join(atual, ".git")).then(() => true, () => false))
|
|
41
|
+
break;
|
|
42
|
+
const pai = dirname(atual);
|
|
43
|
+
if (pai === atual)
|
|
44
|
+
break;
|
|
45
|
+
atual = pai;
|
|
46
|
+
}
|
|
47
|
+
throw new Error(`não encontrei ${CAMINHO_CONFIG} a partir de ${resolve(inicio)}. Configure este projeto com dd-harness start.`);
|
|
48
|
+
}
|
|
49
|
+
/** Uma configuração copiada de outro checkout nunca pode direcionar operações silenciosamente. */
|
|
50
|
+
export async function raizDoMcp() {
|
|
51
|
+
const explicita = process.env.DD_HARNESS_ROOT;
|
|
52
|
+
const atual = await achaRaiz(process.cwd()).catch(() => null);
|
|
53
|
+
if (!explicita)
|
|
54
|
+
return atual ?? process.cwd();
|
|
55
|
+
const raiz = await achaRaiz(explicita);
|
|
56
|
+
const normaliza = (p) => process.platform === "win32" ? resolve(p).toLowerCase() : resolve(p);
|
|
57
|
+
if (atual && normaliza(atual) !== normaliza(raiz)) {
|
|
58
|
+
throw new Error("DD_HARNESS_ROOT aponta para outro checkout/projeto. Corrija a raiz na configuração MCP deste host e reinicie-o.");
|
|
59
|
+
}
|
|
60
|
+
return raiz;
|
|
61
|
+
}
|
|
62
|
+
const arquivoDeCredenciais = () => join(diretorioDoUsuario(), "credentials.json");
|
|
63
|
+
const arquivoDeConfigDaMaquina = () => join(diretorioDoUsuario(), "config.json");
|
|
35
64
|
export async function leConfigDaMaquina() {
|
|
36
65
|
try {
|
|
37
66
|
const cru = await readFile(arquivoDeConfigDaMaquina(), "utf8");
|
|
@@ -43,7 +72,7 @@ export async function leConfigDaMaquina() {
|
|
|
43
72
|
}
|
|
44
73
|
export async function guardaConfigDaMaquina(parcial) {
|
|
45
74
|
const caminho = arquivoDeConfigDaMaquina();
|
|
46
|
-
await mkdir(
|
|
75
|
+
await mkdir(diretorioDoUsuario(), { recursive: true });
|
|
47
76
|
const atual = await leConfigDaMaquina();
|
|
48
77
|
const mesclado = { ...atual, ...parcial };
|
|
49
78
|
await writeFile(caminho, `${JSON.stringify(mesclado, null, 2)}\n`, "utf8");
|
|
@@ -65,7 +94,7 @@ export async function leToken(api) {
|
|
|
65
94
|
}
|
|
66
95
|
export async function guardaToken(api, token) {
|
|
67
96
|
const caminho = arquivoDeCredenciais();
|
|
68
|
-
await mkdir(
|
|
97
|
+
await mkdir(diretorioDoUsuario(), { recursive: true });
|
|
69
98
|
let mapa = {};
|
|
70
99
|
try {
|
|
71
100
|
mapa = JSON.parse(await readFile(caminho, "utf8"));
|
package/dist/curar.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { invalidaCache } from "./cinto.js";
|
|
1
2
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { cabecalhos, credencial, pede, recusa } from "./api.js";
|
|
3
|
+
import { jsonDaEscrita, cabecalhos, credencial, pede, recusa } from "./api.js";
|
|
3
4
|
import { interpreta } from "./gravar.js";
|
|
4
5
|
/**
|
|
5
6
|
* A memoria inteira, no markdown que `gravar` e `editar` consomem.
|
|
@@ -73,6 +74,7 @@ export async function edita(raiz, caminho) {
|
|
|
73
74
|
});
|
|
74
75
|
if (!resposta.ok)
|
|
75
76
|
await recusa(resposta);
|
|
77
|
+
await invalidaCache(raiz);
|
|
76
78
|
return { endereco, ancoras: memoria.ancoras.length };
|
|
77
79
|
}
|
|
78
80
|
export async function arquiva(raiz, endereco, opcoes) {
|
|
@@ -89,7 +91,8 @@ export async function arquiva(raiz, endereco, opcoes) {
|
|
|
89
91
|
});
|
|
90
92
|
if (!resposta.ok)
|
|
91
93
|
await recusa(resposta);
|
|
92
|
-
|
|
94
|
+
await invalidaCache(raiz);
|
|
95
|
+
const lido = (await jsonDaEscrita(resposta));
|
|
93
96
|
return lido;
|
|
94
97
|
}
|
|
95
98
|
/**
|
|
@@ -108,7 +111,8 @@ export async function promove(raiz, endereco, global) {
|
|
|
108
111
|
});
|
|
109
112
|
if (!resposta.ok)
|
|
110
113
|
await recusa(resposta);
|
|
111
|
-
|
|
114
|
+
await invalidaCache(raiz);
|
|
115
|
+
return (await jsonDaEscrita(resposta));
|
|
112
116
|
}
|
|
113
117
|
/**
|
|
114
118
|
* Apaga de verdade, em cascata — ancoras, deriva medida, vinculos, tudo.
|
|
@@ -140,5 +144,6 @@ export async function apaga(raiz, endereco, confirmacao) {
|
|
|
140
144
|
}
|
|
141
145
|
if (!resposta.ok)
|
|
142
146
|
await recusa(resposta);
|
|
143
|
-
|
|
147
|
+
await invalidaCache(raiz);
|
|
148
|
+
return (await jsonDaEscrita(resposta));
|
|
144
149
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { achaRaiz, leConfigDoRepo, leToken } from "./config.js";
|
|
5
|
+
import { buscaPolitica } from "./politica.js";
|
|
6
|
+
export async function handshakeMcp(raiz, comando = "npx", args = ["-y", "dd-harness-mcp@latest"]) {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const filho = spawn(comando, args, { cwd: raiz, windowsHide: true,
|
|
9
|
+
shell: process.platform === "win32" && comando === "npx", stdio: ["pipe", "pipe", "pipe"],
|
|
10
|
+
env: { ...process.env, DD_HARNESS_ROOT: raiz } });
|
|
11
|
+
let buffer = "", terminou = false, versao = "desconhecida";
|
|
12
|
+
const fim = (erro, resultado) => {
|
|
13
|
+
if (terminou)
|
|
14
|
+
return;
|
|
15
|
+
terminou = true;
|
|
16
|
+
clearTimeout(timer);
|
|
17
|
+
filho.stdin.end();
|
|
18
|
+
filho.kill();
|
|
19
|
+
if (erro)
|
|
20
|
+
reject(erro);
|
|
21
|
+
else
|
|
22
|
+
resolve(resultado);
|
|
23
|
+
};
|
|
24
|
+
const timer = setTimeout(() => fim(new Error("MCP não completou initialize/tools/list em 30 segundos")), 30000);
|
|
25
|
+
filho.once("error", () => fim(new Error("Não foi possível iniciar o executável MCP")));
|
|
26
|
+
filho.once("exit", () => { if (!terminou)
|
|
27
|
+
fim(new Error("MCP encerrou antes de listar ferramentas")); });
|
|
28
|
+
filho.stdin.on("error", () => fim(new Error("MCP fechou stdin antes do handshake")));
|
|
29
|
+
filho.stderr.resume(); // Saída do subprocesso pode conter ambiente; não a ecoar.
|
|
30
|
+
const envia = (v) => filho.stdin.write(JSON.stringify(v) + "\n");
|
|
31
|
+
filho.stdout.on("data", chunk => {
|
|
32
|
+
buffer += chunk;
|
|
33
|
+
if (buffer.length > 2000000)
|
|
34
|
+
return fim(new Error("Resposta MCP excedeu o limite do diagnóstico"));
|
|
35
|
+
for (;;) {
|
|
36
|
+
const pos = buffer.indexOf("\n");
|
|
37
|
+
if (pos < 0)
|
|
38
|
+
break;
|
|
39
|
+
const linha = buffer.slice(0, pos);
|
|
40
|
+
buffer = buffer.slice(pos + 1);
|
|
41
|
+
let msg;
|
|
42
|
+
try {
|
|
43
|
+
msg = JSON.parse(linha);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return fim(new Error("MCP imprimiu texto fora do protocolo em stdout"));
|
|
47
|
+
}
|
|
48
|
+
if (msg.error)
|
|
49
|
+
return fim(new Error("MCP recusou initialize/tools/list"));
|
|
50
|
+
if (msg.id === 1) {
|
|
51
|
+
versao = msg.result?.serverInfo?.version ?? versao;
|
|
52
|
+
envia({ jsonrpc: "2.0", method: "notifications/initialized" });
|
|
53
|
+
envia({ jsonrpc: "2.0", id: 2, method: "tools/list" });
|
|
54
|
+
}
|
|
55
|
+
else if (msg.id === 2) {
|
|
56
|
+
const ferramentas = msg.result?.tools?.map((t) => t.name);
|
|
57
|
+
if (!Array.isArray(ferramentas))
|
|
58
|
+
return fim(new Error("tools/list não devolveu ferramentas"));
|
|
59
|
+
fim(undefined, { versao, ferramentas });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
envia({ jsonrpc: "2.0", id: 1, method: "initialize", params: {
|
|
64
|
+
protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "dd-harness-diagnostico", version: "1.0.0" },
|
|
65
|
+
} });
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
export async function diagnostico(inicio, testarMcp = false) {
|
|
69
|
+
const raiz = await achaRaiz(inicio);
|
|
70
|
+
const c = await leConfigDoRepo(raiz);
|
|
71
|
+
const linhas = [`Raiz: ${raiz}`, `Projeto: ${c.tenant}/${c.projeto}`, `Node: ${process.version}`,
|
|
72
|
+
`Credencial: ${await leToken(c.api) ? "presente (valor omitido)" : "ausente"}`];
|
|
73
|
+
for (const [host, config, hook] of [
|
|
74
|
+
["Claude Code", ".mcp.json", ".claude/settings.json"],
|
|
75
|
+
["Codex", ".codex/config.toml", ".codex/hooks.json"],
|
|
76
|
+
["Antigravity", ".agents/mcp_config.json", ".agents/hooks.json"],
|
|
77
|
+
]) {
|
|
78
|
+
const m = await readFile(join(raiz, config), "utf8").catch(() => "");
|
|
79
|
+
const h = await readFile(join(raiz, hook), "utf8").catch(() => "");
|
|
80
|
+
linhas.push(`${host}: MCP ${m.includes("dd-harness") ? "declarado" : "ausente"}; ` +
|
|
81
|
+
`hooks ${h.includes("dd-harness guarda") && h.includes("dd-harness sessao") ? "declarados" : "ausentes/legados"}; ativação no host requer verificação.`);
|
|
82
|
+
if (m.includes("dd-harness") && !m.includes(JSON.stringify(raiz)))
|
|
83
|
+
linhas.push(`${host}: confira DD_HARNESS_ROOT; a raiz explícita atual não foi reconhecida.`);
|
|
84
|
+
}
|
|
85
|
+
const politica = await buscaPolitica(raiz);
|
|
86
|
+
linhas.push(`Contexto: ${politica.estado}${politica.estado === "inalcancavel" ? " — " + politica.motivo : ""}`);
|
|
87
|
+
if (testarMcp) {
|
|
88
|
+
const m = await handshakeMcp(raiz);
|
|
89
|
+
linhas.push(`MCP ${m.versao}: initialize/tools/list concluídos; ${m.ferramentas.length} ferramentas. Isto não comprova hooks ativos.`);
|
|
90
|
+
}
|
|
91
|
+
return linhas;
|
|
92
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type Host = "claude" | "codex" | "antigravity";
|
|
2
|
+
export type Chamada = {
|
|
3
|
+
raiz: string;
|
|
4
|
+
sessao: string;
|
|
5
|
+
ferramenta: string;
|
|
6
|
+
args: Record<string, unknown>;
|
|
7
|
+
};
|
|
8
|
+
export type Edicao = {
|
|
9
|
+
arquivo: string;
|
|
10
|
+
antes?: string;
|
|
11
|
+
depois?: string;
|
|
12
|
+
origem?: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function normalizaEntrada(host: Host, entrada: Record<string, unknown>): Chamada;
|
|
15
|
+
export declare function edicoesDaChamada(c: Chamada): Edicao[];
|