dd-harness 0.2.0 → 0.4.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/LICENSE +11 -11
- package/README.md +64 -64
- package/dist/argv.d.ts +14 -0
- package/dist/argv.js +27 -0
- package/dist/artefato.d.ts +30 -0
- package/dist/artefato.js +42 -0
- package/dist/brain.d.ts +50 -0
- package/dist/brain.js +9 -0
- package/dist/check.d.ts +6 -1
- package/dist/check.js +1 -0
- package/dist/curar.d.ts +38 -0
- package/dist/curar.js +39 -29
- package/dist/diff.d.ts +6 -1
- package/dist/diff.js +7 -1
- package/dist/gravar.js +11 -2
- package/dist/index.js +126 -89
- package/dist/init.d.ts +25 -4
- package/dist/init.js +53 -42
- package/dist/materializa.d.ts +5 -0
- package/dist/politica.d.ts +27 -0
- package/dist/politica.js +35 -0
- package/package.json +2 -2
package/LICENSE
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
Copyright (c) 2026 Diego Dias
|
|
2
|
-
|
|
3
|
-
Todos os direitos reservados.
|
|
4
|
-
|
|
5
|
-
Este software é publicado no registro npm apenas para distribuição ao seu autor e a
|
|
6
|
-
quem ele autorizar expressamente. Nenhuma permissão de uso, cópia, modificação,
|
|
7
|
-
distribuição ou criação de obras derivadas é concedida por esta publicação.
|
|
8
|
-
|
|
9
|
-
All rights reserved. This software is published to the npm registry for distribution
|
|
10
|
-
to its author and to those he expressly authorizes. No permission to use, copy,
|
|
11
|
-
modify, distribute, or create derivative works is granted by this publication.
|
|
1
|
+
Copyright (c) 2026 Diego Dias
|
|
2
|
+
|
|
3
|
+
Todos os direitos reservados.
|
|
4
|
+
|
|
5
|
+
Este software é publicado no registro npm apenas para distribuição ao seu autor e a
|
|
6
|
+
quem ele autorizar expressamente. Nenhuma permissão de uso, cópia, modificação,
|
|
7
|
+
distribuição ou criação de obras derivadas é concedida por esta publicação.
|
|
8
|
+
|
|
9
|
+
All rights reserved. This software is published to the npm registry for distribution
|
|
10
|
+
to its author and to those he expressly authorizes. No permission to use, copy,
|
|
11
|
+
modify, distribute, or create derivative works is granted by this publication.
|
package/README.md
CHANGED
|
@@ -1,64 +1,64 @@
|
|
|
1
|
-
# dd-harness
|
|
2
|
-
|
|
3
|
-
Materializa a **política**, o **briefing** e o **Brain** do
|
|
4
|
-
[dd-harness](https://dd-harness.vercel.app) dentro do seu repositório, para que a sessão
|
|
5
|
-
do agente de código abra já sabendo as regras e as decisões do projeto.
|
|
6
|
-
|
|
7
|
-
Sem dependência: `fetch`, `crypto` e `fs` são do Node. Um CLI que vive pinado em
|
|
8
|
-
repositório alheio precisa envelhecer bem, e cada dependência é uma chance de não
|
|
9
|
-
envelhecer.
|
|
10
|
-
|
|
11
|
-
## Instalação
|
|
12
|
-
|
|
13
|
-
```sh
|
|
14
|
-
npm install -g dd-harness
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
Ou sem instalar nada:
|
|
18
|
-
|
|
19
|
-
```sh
|
|
20
|
-
npx dd-harness sync
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Requer Node 20 ou mais novo.
|
|
24
|
-
|
|
25
|
-
## Uso
|
|
26
|
-
|
|
27
|
-
```sh
|
|
28
|
-
dd-harness init --tenant <espaço> --projeto <projeto> # prepara o repositório
|
|
29
|
-
dd-harness login --token <token> # credencial desta máquina
|
|
30
|
-
dd-harness sync # escreve os artefatos
|
|
31
|
-
dd-harness check [--commit <sha>] # mede âncoras e reporta deriva
|
|
32
|
-
dd-harness status # só lê: o que espera julgamento
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
O token pessoal nasce na tela `/tokens` do serviço. Ele é guardado em
|
|
36
|
-
`~/.dd-harness/credentials.json` (modo `0600`, por origem de API) — **fora** do
|
|
37
|
-
repositório, para não viajar num commit.
|
|
38
|
-
|
|
39
|
-
## O que aparece no seu repositório
|
|
40
|
-
|
|
41
|
-
```
|
|
42
|
-
.dd-harness.json configuração (tenant, projeto, api) — escrita à mão
|
|
43
|
-
CLAUDE.md SEU arquivo; o sync nunca o reescreve
|
|
44
|
-
dd-harness/ tudo o que é gerado
|
|
45
|
-
politica.md
|
|
46
|
-
BRIEFING.md
|
|
47
|
-
brain/
|
|
48
|
-
manifest.json
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
Na raiz fica apenas o **seu** `CLAUDE.md`, que importa a política com a linha
|
|
52
|
-
`@dd-harness/politica.md`. Isso deixa conviverem a parte gerenciada e o que o
|
|
53
|
-
repositório tem de próprio — adotar um projeto existente é acrescentar uma linha, não
|
|
54
|
-
sobrescrever nada.
|
|
55
|
-
|
|
56
|
-
O `sync` confere esse ponteiro a cada execução, inclusive quando o serviço não mudou:
|
|
57
|
-
import quebrado ou linha ausente faz a sessão abrir **sem política e sem avisar**. Isso
|
|
58
|
-
foi medido, não suposto.
|
|
59
|
-
|
|
60
|
-
## Deriva
|
|
61
|
-
|
|
62
|
-
`dd-harness check` mede as âncoras das memórias contra o estado real do repositório e
|
|
63
|
-
reporta o que saiu do lugar. Com `--commit <sha>` ele também cruza as âncoras com o diff
|
|
64
|
-
daquele commit — a memória volta ao code review. Nada bloqueia: avisa.
|
|
1
|
+
# dd-harness
|
|
2
|
+
|
|
3
|
+
Materializa a **política**, o **briefing** e o **Brain** do
|
|
4
|
+
[dd-harness](https://dd-harness.vercel.app) dentro do seu repositório, para que a sessão
|
|
5
|
+
do agente de código abra já sabendo as regras e as decisões do projeto.
|
|
6
|
+
|
|
7
|
+
Sem dependência: `fetch`, `crypto` e `fs` são do Node. Um CLI que vive pinado em
|
|
8
|
+
repositório alheio precisa envelhecer bem, e cada dependência é uma chance de não
|
|
9
|
+
envelhecer.
|
|
10
|
+
|
|
11
|
+
## Instalação
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm install -g dd-harness
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Ou sem instalar nada:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npx dd-harness sync
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Requer Node 20 ou mais novo.
|
|
24
|
+
|
|
25
|
+
## Uso
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
dd-harness init --tenant <espaço> --projeto <projeto> # prepara o repositório
|
|
29
|
+
dd-harness login --token <token> # credencial desta máquina
|
|
30
|
+
dd-harness sync # escreve os artefatos
|
|
31
|
+
dd-harness check [--commit <sha>] # mede âncoras e reporta deriva
|
|
32
|
+
dd-harness status # só lê: o que espera julgamento
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
O token pessoal nasce na tela `/tokens` do serviço. Ele é guardado em
|
|
36
|
+
`~/.dd-harness/credentials.json` (modo `0600`, por origem de API) — **fora** do
|
|
37
|
+
repositório, para não viajar num commit.
|
|
38
|
+
|
|
39
|
+
## O que aparece no seu repositório
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
.dd-harness.json configuração (tenant, projeto, api) — escrita à mão
|
|
43
|
+
CLAUDE.md SEU arquivo; o sync nunca o reescreve
|
|
44
|
+
dd-harness/ tudo o que é gerado
|
|
45
|
+
politica.md
|
|
46
|
+
BRIEFING.md
|
|
47
|
+
brain/
|
|
48
|
+
manifest.json
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Na raiz fica apenas o **seu** `CLAUDE.md`, que importa a política com a linha
|
|
52
|
+
`@dd-harness/politica.md`. Isso deixa conviverem a parte gerenciada e o que o
|
|
53
|
+
repositório tem de próprio — adotar um projeto existente é acrescentar uma linha, não
|
|
54
|
+
sobrescrever nada.
|
|
55
|
+
|
|
56
|
+
O `sync` confere esse ponteiro a cada execução, inclusive quando o serviço não mudou:
|
|
57
|
+
import quebrado ou linha ausente faz a sessão abrir **sem política e sem avisar**. Isso
|
|
58
|
+
foi medido, não suposto.
|
|
59
|
+
|
|
60
|
+
## Deriva
|
|
61
|
+
|
|
62
|
+
`dd-harness check` mede as âncoras das memórias contra o estado real do repositório e
|
|
63
|
+
reporta o que saiu do lugar. Com `--commit <sha>` ele também cruza as âncoras com o diff
|
|
64
|
+
daquele commit — a memória volta ao code review. Nada bloqueia: avisa.
|
package/dist/argv.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Leitura de argumentos da linha de comando.
|
|
3
|
+
*
|
|
4
|
+
* Vive fora do `index.ts` porque aquele modulo executa o CLI ao ser importado
|
|
5
|
+
* (`principal()` no topo) — teste que o importasse rodaria o programa inteiro.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Os termos livres de uma consulta, sem os pares `--flag valor`.
|
|
9
|
+
*
|
|
10
|
+
* Filtrar so o que comeca com `--` deixava o VALOR da flag para tras, e ele entrava na
|
|
11
|
+
* consulta: `buscar "efeito" --limite 2` virava a pergunta `"efeito 2"`. A busca roda, a
|
|
12
|
+
* resposta parece valida e responde a outra coisa — quem chamou nao tem como notar.
|
|
13
|
+
*/
|
|
14
|
+
export declare function termosDaConsulta(argv: string[]): string;
|
package/dist/argv.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Leitura de argumentos da linha de comando.
|
|
3
|
+
*
|
|
4
|
+
* Vive fora do `index.ts` porque aquele modulo executa o CLI ao ser importado
|
|
5
|
+
* (`principal()` no topo) — teste que o importasse rodaria o programa inteiro.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Os termos livres de uma consulta, sem os pares `--flag valor`.
|
|
9
|
+
*
|
|
10
|
+
* Filtrar so o que comeca com `--` deixava o VALOR da flag para tras, e ele entrava na
|
|
11
|
+
* consulta: `buscar "efeito" --limite 2` virava a pergunta `"efeito 2"`. A busca roda, a
|
|
12
|
+
* resposta parece valida e responde a outra coisa — quem chamou nao tem como notar.
|
|
13
|
+
*/
|
|
14
|
+
export function termosDaConsulta(argv) {
|
|
15
|
+
const termos = [];
|
|
16
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
17
|
+
const atual = argv[i];
|
|
18
|
+
if (atual.startsWith("--")) {
|
|
19
|
+
// `--flag=valor` carrega o valor no proprio token; `--flag valor` consome o proximo.
|
|
20
|
+
if (!atual.includes("="))
|
|
21
|
+
i += 1;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
termos.push(atual);
|
|
25
|
+
}
|
|
26
|
+
return termos.join(" ").trim();
|
|
27
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ler e escrever a politica e o briefing do projeto.
|
|
3
|
+
*
|
|
4
|
+
* Ate aqui artefato so se editava pela interface, e isso deixava o agente sem saida para a
|
|
5
|
+
* unica coisa que ele mais precisa saber: a propria politica. O CLI exige que alguem ja
|
|
6
|
+
* tenha escrito nela que o CLI existe — e era justamente ela que estava fora de alcance.
|
|
7
|
+
*/
|
|
8
|
+
export type TipoDeArtefato = "politica" | "briefing";
|
|
9
|
+
/**
|
|
10
|
+
* O `GET` devolve o payload inteiro (politica, briefing, pastas e memorias); aqui so o
|
|
11
|
+
* artefato pedido interessa.
|
|
12
|
+
*
|
|
13
|
+
* `null` e `""` significam a mesma coisa para quem le — "ainda nao existe" —, e a diferenca
|
|
14
|
+
* entre elas e detalhe de como a linha foi parar no banco. Quem chama recebe `existe`, que
|
|
15
|
+
* e a pergunta real.
|
|
16
|
+
*/
|
|
17
|
+
export declare function leArtefato(raiz: string, tipo: TipoDeArtefato): Promise<{
|
|
18
|
+
conteudo: string;
|
|
19
|
+
existe: boolean;
|
|
20
|
+
}>;
|
|
21
|
+
/**
|
|
22
|
+
* Substituicao total, nao append: quem quer acrescentar um paragrafo le, concatena e
|
|
23
|
+
* reenvia. Conteudo vazio e valido e significa "apagar" — a linha fica no banco, guardando
|
|
24
|
+
* quem mexeu por ultimo.
|
|
25
|
+
*/
|
|
26
|
+
export declare function escreveArtefato(raiz: string, tipo: TipoDeArtefato, conteudo: string): Promise<{
|
|
27
|
+
artefato: string;
|
|
28
|
+
criou: boolean;
|
|
29
|
+
tamanho: number;
|
|
30
|
+
}>;
|
package/dist/artefato.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { cabecalhos, credencial, pede, recusa } from "./api.js";
|
|
2
|
+
/**
|
|
3
|
+
* O `GET` devolve o payload inteiro (politica, briefing, pastas e memorias); aqui so o
|
|
4
|
+
* artefato pedido interessa.
|
|
5
|
+
*
|
|
6
|
+
* `null` e `""` significam a mesma coisa para quem le — "ainda nao existe" —, e a diferenca
|
|
7
|
+
* entre elas e detalhe de como a linha foi parar no banco. Quem chama recebe `existe`, que
|
|
8
|
+
* e a pergunta real.
|
|
9
|
+
*/
|
|
10
|
+
export async function leArtefato(raiz, tipo) {
|
|
11
|
+
const { config, token } = await credencial(raiz);
|
|
12
|
+
const url = new URL(`${config.api}/api/v1/artefatos`);
|
|
13
|
+
url.searchParams.set("tenant", config.tenant);
|
|
14
|
+
url.searchParams.set("projeto", config.projeto);
|
|
15
|
+
const resposta = await pede(url, { headers: cabecalhos(token) });
|
|
16
|
+
if (!resposta.ok)
|
|
17
|
+
await recusa(resposta);
|
|
18
|
+
const payload = (await resposta.json());
|
|
19
|
+
const conteudo = typeof payload[tipo] === "string" ? payload[tipo] : "";
|
|
20
|
+
return { conteudo, existe: conteudo.length > 0 };
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Substituicao total, nao append: quem quer acrescentar um paragrafo le, concatena e
|
|
24
|
+
* reenvia. Conteudo vazio e valido e significa "apagar" — a linha fica no banco, guardando
|
|
25
|
+
* quem mexeu por ultimo.
|
|
26
|
+
*/
|
|
27
|
+
export async function escreveArtefato(raiz, tipo, conteudo) {
|
|
28
|
+
const { config, token } = await credencial(raiz);
|
|
29
|
+
const resposta = await pede(`${config.api}/api/v1/artefatos`, {
|
|
30
|
+
method: "PUT",
|
|
31
|
+
headers: cabecalhos(token, true),
|
|
32
|
+
body: JSON.stringify({
|
|
33
|
+
tenant: config.tenant,
|
|
34
|
+
projeto: config.projeto,
|
|
35
|
+
tipo,
|
|
36
|
+
conteudo,
|
|
37
|
+
}),
|
|
38
|
+
});
|
|
39
|
+
if (!resposta.ok)
|
|
40
|
+
await recusa(resposta);
|
|
41
|
+
return (await resposta.json());
|
|
42
|
+
}
|
package/dist/brain.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A forma do Brain no contrato `/api/v1`.
|
|
3
|
+
*
|
|
4
|
+
* So os tipos: o que o servico devolve, e o que o CLI le. Ate a fase 0 este modulo
|
|
5
|
+
* tambem transformava o payload em arquivos de disco — o `sync` materializava politica,
|
|
6
|
+
* briefing e o Brain inteiro no repositorio consumidor. Isso acabou: a politica chega
|
|
7
|
+
* pelo hook de sessao, e a memoria pela busca, na hora. Nada do dd-harness vive em disco.
|
|
8
|
+
*/
|
|
9
|
+
export type Ancora = {
|
|
10
|
+
tipo: string;
|
|
11
|
+
valor: string;
|
|
12
|
+
sha: string | null;
|
|
13
|
+
};
|
|
14
|
+
export type Memoria = {
|
|
15
|
+
pasta: string;
|
|
16
|
+
slug: string;
|
|
17
|
+
titulo: string;
|
|
18
|
+
resumo: string;
|
|
19
|
+
corpo: string;
|
|
20
|
+
status: "ativa" | "historico";
|
|
21
|
+
dano: string;
|
|
22
|
+
invisibilidade: string;
|
|
23
|
+
externalidade: string;
|
|
24
|
+
ancoras: Ancora[];
|
|
25
|
+
revisar_ate: string | null;
|
|
26
|
+
/** Observacoes de deriva esperando julgamento. Ausente em payload antigo. */
|
|
27
|
+
deriva_aberta?: number;
|
|
28
|
+
};
|
|
29
|
+
export type Brain = {
|
|
30
|
+
tenant: {
|
|
31
|
+
slug: string;
|
|
32
|
+
nome: string;
|
|
33
|
+
};
|
|
34
|
+
projeto: {
|
|
35
|
+
slug: string;
|
|
36
|
+
nome: string;
|
|
37
|
+
};
|
|
38
|
+
politica?: string | null;
|
|
39
|
+
briefing?: string | null;
|
|
40
|
+
pastas: {
|
|
41
|
+
slug: string;
|
|
42
|
+
definicao: string;
|
|
43
|
+
}[];
|
|
44
|
+
memorias: Memoria[];
|
|
45
|
+
/**
|
|
46
|
+
* Memorias que excederam as tentativas de indexacao e nao serao mais tentadas. Ficam
|
|
47
|
+
* sem embedding — somem da busca semantica — e so este numero denuncia.
|
|
48
|
+
*/
|
|
49
|
+
travadas_na_fila?: number;
|
|
50
|
+
};
|
package/dist/brain.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A forma do Brain no contrato `/api/v1`.
|
|
3
|
+
*
|
|
4
|
+
* So os tipos: o que o servico devolve, e o que o CLI le. Ate a fase 0 este modulo
|
|
5
|
+
* tambem transformava o payload em arquivos de disco — o `sync` materializava politica,
|
|
6
|
+
* briefing e o Brain inteiro no repositorio consumidor. Isso acabou: a politica chega
|
|
7
|
+
* pelo hook de sessao, e a memoria pela busca, na hora. Nada do dd-harness vive em disco.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
package/dist/check.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Tocada } from "./diff.js";
|
|
2
|
-
import type { Brain } from "./
|
|
2
|
+
import type { Brain } from "./brain.js";
|
|
3
3
|
/**
|
|
4
4
|
* `dd-harness check` — resolve as âncoras contra a árvore de trabalho.
|
|
5
5
|
*
|
|
@@ -51,6 +51,11 @@ export type ResultadoDoStatus = {
|
|
|
51
51
|
memoria: string;
|
|
52
52
|
titulo: string;
|
|
53
53
|
}[];
|
|
54
|
+
/**
|
|
55
|
+
* Memorias que o indexador desistiu de processar. Sem embedding elas somem da busca
|
|
56
|
+
* semantica e continuam aparecendo no acervo — o unico sintoma e procurar e nao achar.
|
|
57
|
+
*/
|
|
58
|
+
travadasNaFila: number;
|
|
54
59
|
};
|
|
55
60
|
/** Separado do IO para poder ser testado sem rede: dado um Brain, o que se mede. */
|
|
56
61
|
export declare function medeAsAncoras(raiz: string, brain: Brain): Promise<Medicao[]>;
|
package/dist/check.js
CHANGED
|
@@ -72,6 +72,7 @@ export async function status(raiz) {
|
|
|
72
72
|
vencidas: ativas
|
|
73
73
|
.filter((m) => m.revisar_ate && new Date(m.revisar_ate).getTime() < agora)
|
|
74
74
|
.map((m) => ({ pasta: m.pasta, memoria: m.slug, titulo: m.titulo })),
|
|
75
|
+
travadasNaFila: brain.travadas_na_fila ?? 0,
|
|
75
76
|
};
|
|
76
77
|
}
|
|
77
78
|
export async function check(raiz, commit) {
|
package/dist/curar.d.ts
CHANGED
|
@@ -1,3 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Curadoria pelo agente: ler, editar e arquivar.
|
|
3
|
+
*
|
|
4
|
+
* `gravar` sabia criar e mais nada. Uma memoria errada ficava errada, porque corrigir
|
|
5
|
+
* exigia abrir a interface — e o `CLAUDE.md` trata curadoria como obrigacao ("se
|
|
6
|
+
* encontrar uma memoria obsoleta ou errada, corrija").
|
|
7
|
+
*
|
|
8
|
+
* Editar reaproveita o mesmo markdown de `gravar`: um formato so para as duas operacoes.
|
|
9
|
+
* Ate a fase 0 o ponto de partida era o arquivo que o `sync` materializava; agora e o
|
|
10
|
+
* `le`, que busca a memoria no servico e devolve nesse mesmo formato.
|
|
11
|
+
*/
|
|
12
|
+
export type MemoriaDoServico = {
|
|
13
|
+
pasta: string;
|
|
14
|
+
slug: string;
|
|
15
|
+
titulo: string;
|
|
16
|
+
resumo: string;
|
|
17
|
+
corpo: string;
|
|
18
|
+
status: string;
|
|
19
|
+
dano: string;
|
|
20
|
+
invisibilidade: string;
|
|
21
|
+
externalidade: string;
|
|
22
|
+
revisar_ate: string | null;
|
|
23
|
+
ancoras: {
|
|
24
|
+
tipo: string;
|
|
25
|
+
valor: string;
|
|
26
|
+
sha: string | null;
|
|
27
|
+
}[];
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* A memoria inteira, no markdown que `gravar` e `editar` consomem.
|
|
31
|
+
*
|
|
32
|
+
* Sem materializacao, esta e a unica forma de ler o corpo: a busca devolve so endereco,
|
|
33
|
+
* titulo e resumo, e o disco nao tem mais nada. Tambem e o ponto de partida de qualquer
|
|
34
|
+
* edicao — corrigir exige ver o que esta la.
|
|
35
|
+
*/
|
|
36
|
+
export declare function le(raiz: string, endereco: string): Promise<string>;
|
|
37
|
+
/** O formato canonico: o mesmo que `interpreta` le, para o ciclo fechar sem conversao. */
|
|
38
|
+
export declare function comoMarkdown(m: MemoriaDoServico): string;
|
|
1
39
|
export declare function edita(raiz: string, caminho: string): Promise<{
|
|
2
40
|
endereco: string;
|
|
3
41
|
ancoras: number;
|
package/dist/curar.js
CHANGED
|
@@ -1,37 +1,48 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { relative } from "node:path";
|
|
3
2
|
import { cabecalhos, credencial, pede, recusa } from "./api.js";
|
|
4
|
-
import { gravaManifesto, hashDe, leManifesto } from "./config.js";
|
|
5
3
|
import { interpreta } from "./gravar.js";
|
|
6
4
|
/**
|
|
7
|
-
*
|
|
5
|
+
* A memoria inteira, no markdown que `gravar` e `editar` consomem.
|
|
8
6
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* Editar reaproveita o mesmo markdown de `gravar`, de proposito: o agente edita o arquivo
|
|
14
|
-
* que o `sync` materializou e manda de volta. Um formato so para as duas operacoes, e o
|
|
15
|
-
* que ele ja sabe ler.
|
|
16
|
-
*/
|
|
17
|
-
/**
|
|
18
|
-
* Depois que o servico aceita, o arquivo em disco deixa de ser "edicao nao enviada" — e o
|
|
19
|
-
* manifesto tem que saber, senao o `sync` seguinte para com "editado a mao" e a unica saida
|
|
20
|
-
* que resta e descartar o arquivo. O ciclo materializa-corrige-envia travava justamente
|
|
21
|
-
* aqui, com o servico ja atualizado.
|
|
22
|
-
*
|
|
23
|
-
* Guarda o hash do que foi enviado, nao do que o servico devolveria: o `sync` seguinte
|
|
24
|
-
* reescreve o arquivo na forma canonica quando o ETag mudar.
|
|
7
|
+
* Sem materializacao, esta e a unica forma de ler o corpo: a busca devolve so endereco,
|
|
8
|
+
* titulo e resumo, e o disco nao tem mais nada. Tambem e o ponto de partida de qualquer
|
|
9
|
+
* edicao — corrigir exige ver o que esta la.
|
|
25
10
|
*/
|
|
26
|
-
async function
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
await
|
|
11
|
+
export async function le(raiz, endereco) {
|
|
12
|
+
const { config, token } = await credencial(raiz);
|
|
13
|
+
const url = new URL(`${config.api}/api/v1/memorias/${endereco}`);
|
|
14
|
+
url.searchParams.set("tenant", config.tenant);
|
|
15
|
+
url.searchParams.set("projeto", config.projeto);
|
|
16
|
+
const resposta = await pede(url, { headers: cabecalhos(token) });
|
|
17
|
+
if (!resposta.ok)
|
|
18
|
+
await recusa(resposta);
|
|
19
|
+
return comoMarkdown((await resposta.json()));
|
|
20
|
+
}
|
|
21
|
+
/** O formato canonico: o mesmo que `interpreta` le, para o ciclo fechar sem conversao. */
|
|
22
|
+
export function comoMarkdown(m) {
|
|
23
|
+
const ancoras = m.ancoras.length
|
|
24
|
+
? `\n## Âncoras\n\n${m.ancoras.map((a) => `- \`${a.valor}\``).join("\n")}\n`
|
|
25
|
+
: "";
|
|
26
|
+
return [
|
|
27
|
+
"---",
|
|
28
|
+
`name: ${m.slug}`,
|
|
29
|
+
`titulo: ${m.titulo}`,
|
|
30
|
+
`description: ${m.resumo}`,
|
|
31
|
+
`pasta: ${m.pasta}`,
|
|
32
|
+
...(m.revisar_ate ? [`revisar-ate: ${m.revisar_ate.slice(0, 10)}`] : []),
|
|
33
|
+
"---",
|
|
34
|
+
"",
|
|
35
|
+
m.corpo.trim(),
|
|
36
|
+
"",
|
|
37
|
+
"## Os três filtros",
|
|
38
|
+
"",
|
|
39
|
+
`**Dano:** ${m.dano}`,
|
|
40
|
+
"",
|
|
41
|
+
`**Invisibilidade:** ${m.invisibilidade}`,
|
|
42
|
+
"",
|
|
43
|
+
`**Externalidade:** ${m.externalidade}`,
|
|
44
|
+
ancoras,
|
|
45
|
+
].join("\n");
|
|
35
46
|
}
|
|
36
47
|
export async function edita(raiz, caminho) {
|
|
37
48
|
const { config, token } = await credencial(raiz);
|
|
@@ -55,7 +66,6 @@ export async function edita(raiz, caminho) {
|
|
|
55
66
|
});
|
|
56
67
|
if (!resposta.ok)
|
|
57
68
|
await recusa(resposta);
|
|
58
|
-
await marcaComoEnviado(raiz, caminho, cru);
|
|
59
69
|
return { endereco, ancoras: memoria.ancoras.length };
|
|
60
70
|
}
|
|
61
71
|
export async function arquiva(raiz, endereco, opcoes) {
|
package/dist/diff.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Brain } from "./
|
|
1
|
+
import type { Brain } from "./brain.js";
|
|
2
2
|
/**
|
|
3
3
|
* Quais memorias falam do que este commit mudou.
|
|
4
4
|
*
|
|
@@ -21,5 +21,10 @@ export type Tocada = {
|
|
|
21
21
|
/**
|
|
22
22
|
* Cruza caminhos com ancoras. A ancora casa quando e o proprio caminho ou quando e um
|
|
23
23
|
* diretorio que o contem — `supabase/migrations` tem que casar com a migration nova.
|
|
24
|
+
*
|
|
25
|
+
* Ancora de trecho (`arquivo.js#alvo`) casa pelo ARQUIVO: o `git diff-tree` devolve
|
|
26
|
+
* caminho puro, nunca com `#`, entao comparar o valor inteiro nao casava nunca — e em
|
|
27
|
+
* silencio, porque nao casar e resultado valido. O alvo continua no `Tocada.ancora`, que
|
|
28
|
+
* e o que a mensagem mostra: quem le precisa saber qual trecho a memoria guarda.
|
|
24
29
|
*/
|
|
25
30
|
export declare function memoriasTocadas(brain: Brain, caminhos: string[]): Tocada[];
|
package/dist/diff.js
CHANGED
|
@@ -30,6 +30,11 @@ export async function caminhosDoCommit(raiz, commit) {
|
|
|
30
30
|
/**
|
|
31
31
|
* Cruza caminhos com ancoras. A ancora casa quando e o proprio caminho ou quando e um
|
|
32
32
|
* diretorio que o contem — `supabase/migrations` tem que casar com a migration nova.
|
|
33
|
+
*
|
|
34
|
+
* Ancora de trecho (`arquivo.js#alvo`) casa pelo ARQUIVO: o `git diff-tree` devolve
|
|
35
|
+
* caminho puro, nunca com `#`, entao comparar o valor inteiro nao casava nunca — e em
|
|
36
|
+
* silencio, porque nao casar e resultado valido. O alvo continua no `Tocada.ancora`, que
|
|
37
|
+
* e o que a mensagem mostra: quem le precisa saber qual trecho a memoria guarda.
|
|
33
38
|
*/
|
|
34
39
|
export function memoriasTocadas(brain, caminhos) {
|
|
35
40
|
const tocadas = [];
|
|
@@ -37,7 +42,8 @@ export function memoriasTocadas(brain, caminhos) {
|
|
|
37
42
|
if (memoria.status !== "ativa")
|
|
38
43
|
continue;
|
|
39
44
|
for (const ancora of memoria.ancoras) {
|
|
40
|
-
const
|
|
45
|
+
const alvo = ancora.valor.split("#")[0];
|
|
46
|
+
const casa = caminhos.some((c) => c === alvo || c.startsWith(`${alvo}/`));
|
|
41
47
|
if (casa) {
|
|
42
48
|
tocadas.push({
|
|
43
49
|
pasta: memoria.pasta,
|
package/dist/gravar.js
CHANGED
|
@@ -46,9 +46,18 @@ export function interpreta(texto) {
|
|
|
46
46
|
throw new Error(`frontmatter sem \`${chave}\`.`);
|
|
47
47
|
}
|
|
48
48
|
// O corpo vai ate a secao dos filtros; dali para baixo e metadado, nao conteudo.
|
|
49
|
-
const
|
|
50
|
-
|
|
49
|
+
const cabecalhoDosFiltros = /\n##\s+Os tr[êe]s filtros\s*\n/gi;
|
|
50
|
+
const ocorrencias = [...resto.matchAll(cabecalhoDosFiltros)];
|
|
51
|
+
if (ocorrencias.length === 0)
|
|
51
52
|
throw new Error("falta a seção `## Os três filtros`.");
|
|
53
|
+
// Duas ocorrencias tornam o arquivo ambiguo: cortar na primeira trunca o corpo (e os
|
|
54
|
+
// filtros ainda saem certos da segunda, entao grava e perde dado sem avisar), e cortar
|
|
55
|
+
// na ultima escolheria em silencio qual secao e a de verdade. Recusar diz o que houve.
|
|
56
|
+
if (ocorrencias.length > 1) {
|
|
57
|
+
throw new Error("a seção `## Os três filtros` aparece mais de uma vez — deixe só a real, " +
|
|
58
|
+
"ou reescreva a citação no corpo (por exemplo, entre crases).");
|
|
59
|
+
}
|
|
60
|
+
const corte = ocorrencias[0].index;
|
|
52
61
|
const corpo = resto
|
|
53
62
|
.slice(0, corte)
|
|
54
63
|
.replace(/<!--[\s\S]*?-->/g, "")
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { termosDaConsulta } from "./argv.js";
|
|
2
3
|
import { check, status } from "./check.js";
|
|
3
4
|
import { leConfigDoRepo, guardaToken } from "./config.js";
|
|
4
5
|
import { grava } from "./gravar.js";
|
|
5
|
-
import { init, SUGESTAO_MCP } from "./init.js";
|
|
6
|
-
import {
|
|
6
|
+
import { init, SUGESTAO_HOOK, SUGESTAO_MCP } from "./init.js";
|
|
7
|
+
import { buscaPolitica } from "./politica.js";
|
|
7
8
|
import { busca } from "./buscar.js";
|
|
8
|
-
import { arquiva, edita } from "./curar.js";
|
|
9
|
+
import { arquiva, edita, le } from "./curar.js";
|
|
9
10
|
import { criaPasta } from "./pasta.js";
|
|
10
11
|
import { criaProjeto } from "./projeto.js";
|
|
11
|
-
import { sync } from "./sync.js";
|
|
12
12
|
/**
|
|
13
13
|
* `dd-harness` — o cliente que materializa os artefatos no repositorio.
|
|
14
14
|
*
|
|
@@ -17,7 +17,7 @@ import { sync } from "./sync.js";
|
|
|
17
17
|
* em repositorio alheio tem que envelhecer bem, e cada dependencia e uma chance de nao
|
|
18
18
|
* envelhecer.
|
|
19
19
|
*/
|
|
20
|
-
const AJUDA = `dd-harness —
|
|
20
|
+
const AJUDA = `dd-harness — a política e o Brain do projeto, no serviço
|
|
21
21
|
|
|
22
22
|
dd-harness login --token <token> [--api <url>]
|
|
23
23
|
guarda a credencial desta máquina
|
|
@@ -33,61 +33,35 @@ const AJUDA = `dd-harness — materializa política, briefing e Brain no reposit
|
|
|
33
33
|
[--substituida-por <pasta>/<slug>]
|
|
34
34
|
tira de circulação sem apagar
|
|
35
35
|
dd-harness buscar "<pergunta>" acha memória por relevância, não por arquivo
|
|
36
|
-
dd-harness
|
|
36
|
+
dd-harness ler <pasta>/<slug> imprime a memória inteira, no formato de gravar
|
|
37
37
|
dd-harness check [--commit <sha>] mede as âncoras e reporta a deriva
|
|
38
38
|
dd-harness status só lê: o tamanho do Brain e o que espera julgamento
|
|
39
|
+
dd-harness politica [--hook] imprime a política do serviço
|
|
40
|
+
saída 0 = veio; 3 = projeto sem política;
|
|
41
|
+
1 = não consegui buscar
|
|
42
|
+
--hook: fala o protocolo do SessionStart do
|
|
43
|
+
Claude Code, para pôr a política no contexto
|
|
39
44
|
dd-harness --help
|
|
40
45
|
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
Nada do dd-harness fica em disco: a política chega pelo hook de sessão, e a
|
|
47
|
+
memória pela busca, na hora.
|
|
43
48
|
`;
|
|
44
|
-
/** O aviso existe porque o import falha em silêncio — foi medido, não suposto. */
|
|
45
|
-
function avisaSobreOPonteiro(ponteiro) {
|
|
46
|
-
if (ponteiro === "ok" || ponteiro === "sem-politica")
|
|
47
|
-
return;
|
|
48
|
-
// O import pendurado e o inverso dos outros dois: a linha esta la, o alvo e que nao
|
|
49
|
-
// existe. Dizer "acrescente a linha" aqui mandaria a pessoa para o lugar errado.
|
|
50
|
-
if (ponteiro === "aponta-para-o-vazio") {
|
|
51
|
-
console.error([
|
|
52
|
-
"",
|
|
53
|
-
`AVISO: o CLAUDE.md importa ${LINHA_DE_IMPORT}, mas não há política no serviço.`,
|
|
54
|
-
"O arquivo apontado não existe, e import quebrado falha em silêncio: a sessão abre",
|
|
55
|
-
"sem protocolo e nada avisa.",
|
|
56
|
-
"",
|
|
57
|
-
"Escreva a política do projeto no serviço, ou tire a linha do CLAUDE.md.",
|
|
58
|
-
].join("\n"));
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
const motivo = ponteiro === "sem-claude-md"
|
|
62
|
-
? "não há CLAUDE.md na raiz"
|
|
63
|
-
: "o CLAUDE.md da raiz não importa a política";
|
|
64
|
-
console.error([
|
|
65
|
-
"",
|
|
66
|
-
`AVISO: ${motivo}.`,
|
|
67
|
-
"A política existe no serviço e está em disco, mas não chega à sessão: o import",
|
|
68
|
-
"ausente falha em silêncio, e a sessão abre sem protocolo sem avisar ninguém.",
|
|
69
|
-
"",
|
|
70
|
-
`Acrescente esta linha ao CLAUDE.md da raiz: ${LINHA_DE_IMPORT}`,
|
|
71
|
-
"Ou rode: dd-harness init --tenant <t> --projeto <p>",
|
|
72
|
-
].join("\n"));
|
|
73
|
-
}
|
|
74
49
|
/**
|
|
75
50
|
* Os ganchos sao IMPRESSOS, nunca instalados. `.git/hooks` nao e versionado e nao e
|
|
76
51
|
* nosso: escrever la dentro sem a pessoa pedir e o mesmo tipo de invasao que
|
|
77
52
|
* sobrescrever o CLAUDE.md dela. Quem cola, decide.
|
|
78
53
|
*/
|
|
79
54
|
const GANCHOS = `
|
|
80
|
-
Opcional —
|
|
55
|
+
Opcional — o gancho que devolve a memória ao code review:
|
|
81
56
|
|
|
82
57
|
.git/hooks/post-commit (avisa quais memórias falam do que você mudou)
|
|
83
58
|
#!/bin/sh
|
|
84
59
|
dd-harness check --commit "$(git rev-parse HEAD)" || true
|
|
85
60
|
|
|
86
|
-
|
|
87
|
-
"hooks": { "SessionStart": [{ "hooks": [{ "type": "command",
|
|
88
|
-
"command": "dd-harness status" }] }] }
|
|
61
|
+
Termina em sucesso mesmo com deriva: avisa, não bloqueia.
|
|
89
62
|
|
|
90
|
-
|
|
63
|
+
O hook da política (\`dd-harness politica --hook\`) é outra coisa, e não é
|
|
64
|
+
opcional — \`dd-harness init\` imprime a linha para o \`.claude/settings.json\`.`;
|
|
91
65
|
function argumento(argv, nome) {
|
|
92
66
|
const i = argv.indexOf(`--${nome}`);
|
|
93
67
|
return i >= 0 ? argv[i + 1] : undefined;
|
|
@@ -104,12 +78,19 @@ async function comandoInit(argv) {
|
|
|
104
78
|
api: argumento(argv, "api"),
|
|
105
79
|
});
|
|
106
80
|
console.log(r.config === "criada" ? "criado .dd-harness.json" : "mantido .dd-harness.json");
|
|
107
|
-
console.log(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
81
|
+
console.log("\nAgora: dd-harness login --token <token>");
|
|
82
|
+
// O hook vem primeiro e nao e opcional: sem ele a sessao abre sem politica, que e a
|
|
83
|
+
// falha que este projeto existe para combater. O MCP e conveniencia; este, nao.
|
|
84
|
+
if (r.hook === "ja-declarado") {
|
|
85
|
+
console.log("\nmantido .claude/settings.json — o hook da política já está declarado");
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
console.log("\nOBRIGATÓRIO: o hook que carrega a política no início de cada sessão." +
|
|
89
|
+
"\nSem ele a sessão abre sem protocolo, e nada avisa. Acrescente ao" +
|
|
90
|
+
"\n`.claude/settings.json` (não escrevo nele: o arquivo é seu e pode já" +
|
|
91
|
+
"\nter hooks e permissões):\n");
|
|
92
|
+
console.log(SUGESTAO_HOOK);
|
|
93
|
+
}
|
|
113
94
|
if (r.mcp === "ja-declarado") {
|
|
114
95
|
console.log("\nmantido .mcp.json — o servidor dd-harness já está declarado");
|
|
115
96
|
return;
|
|
@@ -142,7 +123,7 @@ async function comandoEditar(argv) {
|
|
|
142
123
|
}
|
|
143
124
|
const r = await edita(process.cwd(), caminho);
|
|
144
125
|
console.log(`editado ${r.endereco}`);
|
|
145
|
-
console.log(` ${r.ancoras} âncora(s)
|
|
126
|
+
console.log(` ${r.ancoras} âncora(s).`);
|
|
146
127
|
}
|
|
147
128
|
const MOTIVOS = ["obsoleta", "incorreta", "fora_dos_filtros"];
|
|
148
129
|
async function comandoArquivar(argv) {
|
|
@@ -161,10 +142,23 @@ async function comandoArquivar(argv) {
|
|
|
161
142
|
substituidaPor: argumento(argv, "substituida-por"),
|
|
162
143
|
});
|
|
163
144
|
console.log(`arquivado ${r.endereco} (${r.motivo})`);
|
|
164
|
-
console.log(" foi para o histórico, não foi apagada.
|
|
145
|
+
console.log(" foi para o histórico, não foi apagada.");
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* A memoria inteira no stdout, no mesmo markdown que `gravar` e `editar` consomem.
|
|
149
|
+
*
|
|
150
|
+
* Sem materializacao este e o unico caminho para o corpo: a busca devolve so endereco,
|
|
151
|
+
* titulo e resumo. Tambem e o ponto de partida de toda edicao — corrigir exige ver.
|
|
152
|
+
*/
|
|
153
|
+
async function comandoLer(argv) {
|
|
154
|
+
const endereco = argv[0];
|
|
155
|
+
if (!endereco || endereco.startsWith("-")) {
|
|
156
|
+
throw new Error("uso: dd-harness ler <pasta>/<slug>");
|
|
157
|
+
}
|
|
158
|
+
console.log(await le(process.cwd(), endereco));
|
|
165
159
|
}
|
|
166
160
|
async function comandoBuscar(argv) {
|
|
167
|
-
const consulta = argv
|
|
161
|
+
const consulta = termosDaConsulta(argv);
|
|
168
162
|
if (!consulta)
|
|
169
163
|
throw new Error('uso: dd-harness buscar "<pergunta>"');
|
|
170
164
|
const limite = Number(argumento(argv, "limite")) || undefined;
|
|
@@ -186,36 +180,6 @@ async function comandoBuscar(argv) {
|
|
|
186
180
|
console.log(` ${a.resumo}`);
|
|
187
181
|
}
|
|
188
182
|
}
|
|
189
|
-
async function comandoSync() {
|
|
190
|
-
const resultado = await sync(process.cwd());
|
|
191
|
-
if (resultado.tipo === "editado-a-mao") {
|
|
192
|
-
console.error([
|
|
193
|
-
"parei sem escrever nada: estes arquivos foram editados à mão.",
|
|
194
|
-
...resultado.arquivos.map((a) => ` ${a}`),
|
|
195
|
-
"",
|
|
196
|
-
"O disco é projeção do serviço, uma direção só. Duas saídas:",
|
|
197
|
-
"",
|
|
198
|
-
" 1. Leve a edição para o serviço — `dd-harness editar <arquivo>` para cada um",
|
|
199
|
-
" acima. É o caminho normal de corrigir memória, e destrava o sync.",
|
|
200
|
-
" 2. Descarte a edição local (git checkout / apague o arquivo) e sincronize.",
|
|
201
|
-
].join("\n"));
|
|
202
|
-
process.exitCode = 1;
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
if (resultado.tipo === "sem-mudanca") {
|
|
206
|
-
console.log("nada mudou no serviço — disco já está em dia.");
|
|
207
|
-
}
|
|
208
|
-
else {
|
|
209
|
-
for (const a of resultado.escritos)
|
|
210
|
-
console.log(`escrito ${a}`);
|
|
211
|
-
for (const a of resultado.removidos)
|
|
212
|
-
console.log(`removido ${a}`);
|
|
213
|
-
if (!resultado.escritos.length && !resultado.removidos.length) {
|
|
214
|
-
console.log("conteúdo novo do serviço, sem diferença em disco.");
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
avisaSobreOPonteiro(resultado.ponteiro);
|
|
218
|
-
}
|
|
219
183
|
async function comandoCheck(argv) {
|
|
220
184
|
const r = await check(process.cwd(), argumento(argv, "commit"));
|
|
221
185
|
if (r.medidas === 0) {
|
|
@@ -261,6 +225,71 @@ async function comandoCheck(argv) {
|
|
|
261
225
|
}
|
|
262
226
|
}
|
|
263
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* Imprime a politica no stdout, para o hook `SessionStart` injetar no contexto.
|
|
230
|
+
*
|
|
231
|
+
* Os codigos de saida sao o contrato com o hook, e existem para separar duas coisas que
|
|
232
|
+
* um "falhou" generico colapsaria: projeto novo (que precisa de briefing) de politica
|
|
233
|
+
* inalcancavel (que precisa PARAR a sessao). Mudar estes numeros quebra o hook.
|
|
234
|
+
*/
|
|
235
|
+
async function comandoPolitica(argv) {
|
|
236
|
+
const r = await buscaPolitica(process.cwd());
|
|
237
|
+
// `--hook`: fala o protocolo do SessionStart do Claude Code, que injeta
|
|
238
|
+
// `additionalContext` no contexto da sessao. Sem a flag, saida legivel para quem roda
|
|
239
|
+
// no terminal. A diferenca importa: o hook precisa que o AVISO chegue ao modelo, e
|
|
240
|
+
// stderr so chega ao transcript — aviso que o modelo nao le e o mesmo que silencio.
|
|
241
|
+
if (argv.includes("--hook")) {
|
|
242
|
+
console.log(JSON.stringify({ hookSpecificOutput: contextoDaSessao(r) }));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (r.estado === "ok") {
|
|
246
|
+
console.log(r.conteudo);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (r.estado === "sem-politica") {
|
|
250
|
+
console.error("Este projeto ainda não tem política — nunca foi briefado. Rode `/briefar`.");
|
|
251
|
+
process.exitCode = 3;
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
console.error(`não consegui buscar a política: ${r.motivo}`);
|
|
255
|
+
process.exitCode = 1;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* O que o hook injeta no contexto, por estado.
|
|
259
|
+
*
|
|
260
|
+
* Sai sempre com codigo 0: o que precisa chegar ao modelo e o TEXTO, e um codigo de erro
|
|
261
|
+
* so faria o Claude Code registrar falha no transcript — que ninguem le — enquanto a
|
|
262
|
+
* sessao seguiria sem saber que esta sem protocolo.
|
|
263
|
+
*/
|
|
264
|
+
function contextoDaSessao(r) {
|
|
265
|
+
const base = { hookEventName: "SessionStart" };
|
|
266
|
+
if (r.estado === "ok") {
|
|
267
|
+
return {
|
|
268
|
+
...base,
|
|
269
|
+
additionalContext: "# Política deste projeto (carregada do dd-harness)\n\n" +
|
|
270
|
+
"As regras abaixo valem para esta sessão inteira.\n\n" +
|
|
271
|
+
r.conteudo,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
if (r.estado === "sem-politica") {
|
|
275
|
+
return {
|
|
276
|
+
...base,
|
|
277
|
+
additionalContext: "AVISO DO DD-HARNESS: este projeto existe no serviço mas **nunca foi briefado** " +
|
|
278
|
+
"— não há política.\n\nIsto não é uma falha: é um projeto novo. Antes de " +
|
|
279
|
+
"implementar qualquer coisa, diga isso ao usuário e proponha rodar `/briefar`.",
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
...base,
|
|
284
|
+
additionalContext: "PARE: NÃO FOI POSSÍVEL CARREGAR A POLÍTICA DESTE PROJETO.\n\n" +
|
|
285
|
+
`Motivo: ${r.motivo}\n\n` +
|
|
286
|
+
"A política pode existir no serviço e não ter chegado até aqui, então esta sessão " +
|
|
287
|
+
"está **sem protocolo** — as proibições e a regra do OK não foram carregadas.\n\n" +
|
|
288
|
+
"Antes de qualquer outra coisa: avise o usuário com estas palavras e **não " +
|
|
289
|
+
"modifique nenhum arquivo** até ele decidir como prosseguir. Seguir como se nada " +
|
|
290
|
+
"tivesse acontecido é exatamente a falha que este projeto combate.",
|
|
291
|
+
};
|
|
292
|
+
}
|
|
264
293
|
async function comandoStatus() {
|
|
265
294
|
const r = await status(process.cwd());
|
|
266
295
|
// O acervo vem primeiro, e sempre: e a resposta para "o que ha no Brain deste projeto?",
|
|
@@ -276,6 +305,14 @@ async function comandoStatus() {
|
|
|
276
305
|
(arquivadas ? ` e ${arquivadas} arquivada(s)` : "") +
|
|
277
306
|
(detalhe ? ` — ${detalhe}` : ""));
|
|
278
307
|
}
|
|
308
|
+
// Antes do early return abaixo: fila travada nao e "julgamento esperando", e sumiria
|
|
309
|
+
// justamente na sessao mais comum — a que nao tem deriva nem vencida.
|
|
310
|
+
if (r.travadasNaFila > 0) {
|
|
311
|
+
console.log("");
|
|
312
|
+
console.log(`AVISO: ${r.travadasNaFila} memória(s) desistiram de ser indexadas e não entram na busca semântica.`);
|
|
313
|
+
console.log(" O indexador tentou 5 vezes e parou. Rode `pnpm worker --reindexar`;");
|
|
314
|
+
console.log(" se repetir, o motivo está em `embedding_queue.ultimo_erro`.");
|
|
315
|
+
}
|
|
279
316
|
if (!r.comDeriva.length && !r.vencidas.length) {
|
|
280
317
|
console.log("Nada esperando julgamento.");
|
|
281
318
|
return;
|
|
@@ -327,9 +364,8 @@ async function comandoPasta(argv) {
|
|
|
327
364
|
}
|
|
328
365
|
/**
|
|
329
366
|
* O agente escreve o arquivo — que e o que ele ja fazia no modelo file-based — e este
|
|
330
|
-
* comando o transforma em requisicao. O arquivo
|
|
331
|
-
*
|
|
332
|
-
* lado da copia gerada.
|
|
367
|
+
* comando o transforma em requisicao. O arquivo e so o veiculo: depois de gravado, a
|
|
368
|
+
* memoria vive no servico, e quem quiser le-la usa a busca. Nada fica em disco.
|
|
333
369
|
*/
|
|
334
370
|
async function comandoGravar(argv) {
|
|
335
371
|
const caminho = argv[0];
|
|
@@ -338,8 +374,7 @@ async function comandoGravar(argv) {
|
|
|
338
374
|
}
|
|
339
375
|
const r = await grava(process.cwd(), caminho);
|
|
340
376
|
console.log(`gravado ${r.endereco}`);
|
|
341
|
-
console.log(` ${r.ancoras} âncora(s), valendo para ${r.projetos} projeto(s).`
|
|
342
|
-
" Rode `dd-harness sync` para materializar.");
|
|
377
|
+
console.log(` ${r.ancoras} âncora(s), valendo para ${r.projetos} projeto(s).`);
|
|
343
378
|
}
|
|
344
379
|
async function principal() {
|
|
345
380
|
const [comando, ...resto] = process.argv.slice(2);
|
|
@@ -358,14 +393,16 @@ async function principal() {
|
|
|
358
393
|
return comandoEditar(resto);
|
|
359
394
|
case "arquivar":
|
|
360
395
|
return comandoArquivar(resto);
|
|
396
|
+
case "ler":
|
|
397
|
+
return comandoLer(resto);
|
|
361
398
|
case "buscar":
|
|
362
399
|
return comandoBuscar(resto);
|
|
363
|
-
case "sync":
|
|
364
|
-
return comandoSync();
|
|
365
400
|
case "check":
|
|
366
401
|
return comandoCheck(resto);
|
|
367
402
|
case "status":
|
|
368
403
|
return comandoStatus();
|
|
404
|
+
case "politica":
|
|
405
|
+
return comandoPolitica(resto);
|
|
369
406
|
case "--help":
|
|
370
407
|
case "-h":
|
|
371
408
|
case undefined:
|
package/dist/init.d.ts
CHANGED
|
@@ -1,16 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prepara um repositorio para o dd-harness.
|
|
3
|
+
*
|
|
4
|
+
* Escreve UM arquivo: o `.dd-harness.json`, que diz a que projeto este repositorio
|
|
5
|
+
* pertence. Todo o resto e sugestao impressa, para quem cola decidir.
|
|
6
|
+
*
|
|
7
|
+
* Ate a fase 0 este comando tambem escrevia uma linha de import no `CLAUDE.md`, que
|
|
8
|
+
* apontava para a politica materializada em disco. Isso acabou: a politica chega pelo
|
|
9
|
+
* hook de sessao, e nada do dd-harness fica em disco.
|
|
10
|
+
*/
|
|
1
11
|
export type ResultadoDoInit = {
|
|
2
12
|
config: "criada" | "ja-existia";
|
|
3
|
-
claudeMd: "criado" | "linha-acrescentada" | "ja-tinha-a-linha";
|
|
4
13
|
mcp: "ja-declarado" | "a-declarar";
|
|
14
|
+
hook: "ja-declarado" | "a-declarar";
|
|
5
15
|
};
|
|
6
16
|
/**
|
|
7
17
|
* O `.mcp.json` e sugerido, nunca escrito.
|
|
8
18
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
19
|
+
* O arquivo e do repositorio, pode ja declarar outros servidores, e mesclar JSON alheio e
|
|
20
|
+
* onde falha silenciosa nasce — entrada errada nao da erro, a ferramenta so nao aparece.
|
|
21
|
+
* Quem cola sabe o que colou.
|
|
12
22
|
*/
|
|
13
23
|
export declare const SUGESTAO_MCP = "{\n \"mcpServers\": {\n \"dd-harness\": {\n \"command\": \"npx\",\n \"args\": [\"-y\", \"dd-harness-mcp\"]\n }\n }\n}";
|
|
24
|
+
/**
|
|
25
|
+
* O hook que carrega a politica no inicio de cada sessao.
|
|
26
|
+
*
|
|
27
|
+
* E a garantia de que nenhuma sessao abre sem protocolo — o papel que antes era do
|
|
28
|
+
* arquivo materializado mais a linha de import. Vive num hook, e nao numa instrucao no
|
|
29
|
+
* `CLAUDE.md`, porque instrucao o modelo pode pular: o import quebrado falhava em
|
|
30
|
+
* silencio, e isso foi medido.
|
|
31
|
+
*
|
|
32
|
+
* Sugerido e nao escrito, pelo mesmo motivo do `.mcp.json`.
|
|
33
|
+
*/
|
|
34
|
+
export declare const SUGESTAO_HOOK = "{\n \"hooks\": {\n \"SessionStart\": [\n {\n \"hooks\": [\n {\n \"type\": \"command\",\n \"command\": \"dd-harness politica --hook\",\n \"statusMessage\": \"Carregando a pol\u00EDtica do dd-harness...\"\n }\n ]\n }\n ]\n }\n}";
|
|
14
35
|
export declare function init(raiz: string, dados: {
|
|
15
36
|
tenant: string;
|
|
16
37
|
projeto: string;
|
package/dist/init.js
CHANGED
|
@@ -1,28 +1,12 @@
|
|
|
1
1
|
import { readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { CAMINHO_CONFIG } from "./config.js";
|
|
4
|
-
import { LINHA_DE_IMPORT } from "./materializa.js";
|
|
5
|
-
/**
|
|
6
|
-
* Prepara um repositorio para o dd-harness.
|
|
7
|
-
*
|
|
8
|
-
* Funciona nos dois casos, e o segundo e o que importa para a ambicao de adotar projeto
|
|
9
|
-
* que ja existe: se nao ha `CLAUDE.md`, cria um com a linha de import; se **ja ha**,
|
|
10
|
-
* acrescenta a linha ao que voce escreveu, sem tocar no resto. Adotar um projeto vira
|
|
11
|
-
* uma linha, e nao um ritual de mover arquivo.
|
|
12
|
-
*/
|
|
13
|
-
const CABECALHO = `# CLAUDE.md
|
|
14
|
-
|
|
15
|
-
Este arquivo é seu: escreva aqui o que for específico deste repositório.
|
|
16
|
-
|
|
17
|
-
A linha abaixo importa a política gerenciada pelo dd-harness. **Não a remova** — sem
|
|
18
|
-
ela a sessão abre sem protocolo, e nada avisa.
|
|
19
|
-
`;
|
|
20
4
|
/**
|
|
21
5
|
* O `.mcp.json` e sugerido, nunca escrito.
|
|
22
6
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
7
|
+
* O arquivo e do repositorio, pode ja declarar outros servidores, e mesclar JSON alheio e
|
|
8
|
+
* onde falha silenciosa nasce — entrada errada nao da erro, a ferramenta so nao aparece.
|
|
9
|
+
* Quem cola sabe o que colou.
|
|
26
10
|
*/
|
|
27
11
|
export const SUGESTAO_MCP = `{
|
|
28
12
|
"mcpServers": {
|
|
@@ -32,6 +16,31 @@ export const SUGESTAO_MCP = `{
|
|
|
32
16
|
}
|
|
33
17
|
}
|
|
34
18
|
}`;
|
|
19
|
+
/**
|
|
20
|
+
* O hook que carrega a politica no inicio de cada sessao.
|
|
21
|
+
*
|
|
22
|
+
* E a garantia de que nenhuma sessao abre sem protocolo — o papel que antes era do
|
|
23
|
+
* arquivo materializado mais a linha de import. Vive num hook, e nao numa instrucao no
|
|
24
|
+
* `CLAUDE.md`, porque instrucao o modelo pode pular: o import quebrado falhava em
|
|
25
|
+
* silencio, e isso foi medido.
|
|
26
|
+
*
|
|
27
|
+
* Sugerido e nao escrito, pelo mesmo motivo do `.mcp.json`.
|
|
28
|
+
*/
|
|
29
|
+
export const SUGESTAO_HOOK = `{
|
|
30
|
+
"hooks": {
|
|
31
|
+
"SessionStart": [
|
|
32
|
+
{
|
|
33
|
+
"hooks": [
|
|
34
|
+
{
|
|
35
|
+
"type": "command",
|
|
36
|
+
"command": "dd-harness politica --hook",
|
|
37
|
+
"statusMessage": "Carregando a política do dd-harness..."
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
}`;
|
|
35
44
|
async function declaraMcp(raiz) {
|
|
36
45
|
try {
|
|
37
46
|
const cru = await readFile(join(raiz, ".mcp.json"), "utf8");
|
|
@@ -43,6 +52,26 @@ async function declaraMcp(raiz) {
|
|
|
43
52
|
return "a-declarar";
|
|
44
53
|
}
|
|
45
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* O hook ja esta declarado?
|
|
57
|
+
*
|
|
58
|
+
* Procura pelo COMANDO, nao pela forma: `settings.json` aceita varios formatos de
|
|
59
|
+
* matcher, e quem ja tem o hook pode te-lo escrito de outro jeito. O que importa e se
|
|
60
|
+
* `dd-harness politica` roda no inicio da sessao.
|
|
61
|
+
*/
|
|
62
|
+
async function declaraHook(raiz) {
|
|
63
|
+
for (const arquivo of [".claude/settings.json", ".claude/settings.local.json"]) {
|
|
64
|
+
try {
|
|
65
|
+
const cru = await readFile(join(raiz, arquivo), "utf8");
|
|
66
|
+
if (cru.includes("dd-harness politica"))
|
|
67
|
+
return "ja-declarado";
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Sem arquivo: segue para o proximo.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return "a-declarar";
|
|
74
|
+
}
|
|
46
75
|
export async function init(raiz, dados) {
|
|
47
76
|
const caminhoConfig = join(raiz, CAMINHO_CONFIG);
|
|
48
77
|
let config = "ja-existia";
|
|
@@ -58,27 +87,9 @@ export async function init(raiz, dados) {
|
|
|
58
87
|
await writeFile(caminhoConfig, `${JSON.stringify(conteudo, null, 2)}\n`, "utf8");
|
|
59
88
|
config = "criada";
|
|
60
89
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
catch {
|
|
68
|
-
atual = null;
|
|
69
|
-
}
|
|
70
|
-
if (atual === null) {
|
|
71
|
-
await writeFile(caminhoClaude, `${CABECALHO}\n${LINHA_DE_IMPORT}\n`, "utf8");
|
|
72
|
-
claudeMd = "criado";
|
|
73
|
-
}
|
|
74
|
-
else if (atual.includes(LINHA_DE_IMPORT)) {
|
|
75
|
-
claudeMd = "ja-tinha-a-linha";
|
|
76
|
-
}
|
|
77
|
-
else {
|
|
78
|
-
// Acrescenta no fim, sem reescrever nada do que ja estava la.
|
|
79
|
-
const separador = atual.endsWith("\n") ? "\n" : "\n\n";
|
|
80
|
-
await writeFile(caminhoClaude, `${atual}${separador}${LINHA_DE_IMPORT}\n`, "utf8");
|
|
81
|
-
claudeMd = "linha-acrescentada";
|
|
82
|
-
}
|
|
83
|
-
return { config, claudeMd, mcp: await declaraMcp(raiz) };
|
|
90
|
+
return {
|
|
91
|
+
config,
|
|
92
|
+
mcp: await declaraMcp(raiz),
|
|
93
|
+
hook: await declaraHook(raiz),
|
|
94
|
+
};
|
|
84
95
|
}
|
package/dist/materializa.d.ts
CHANGED
|
@@ -44,6 +44,11 @@ export type Brain = {
|
|
|
44
44
|
definicao: string;
|
|
45
45
|
}[];
|
|
46
46
|
memorias: Memoria[];
|
|
47
|
+
/**
|
|
48
|
+
* Memorias que excederam as tentativas de indexacao e nao serao mais tentadas. Ficam
|
|
49
|
+
* sem embedding — somem da busca semantica — e so este numero denuncia.
|
|
50
|
+
*/
|
|
51
|
+
travadas_na_fila?: number;
|
|
47
52
|
};
|
|
48
53
|
export declare function arquivoDaMemoria(m: Memoria): string;
|
|
49
54
|
export declare function arquivoDoIndice(brain: Brain): string;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A politica do projeto, buscada no servico na hora.
|
|
3
|
+
*
|
|
4
|
+
* Existe para o hook `SessionStart`: sem materializacao em disco, e ele quem garante que
|
|
5
|
+
* nenhuma sessao abre sem protocolo. Por isso o retorno separa TRES situacoes que um
|
|
6
|
+
* "deu erro" colapsaria — e cada uma pede uma reacao diferente de quem chamou:
|
|
7
|
+
*
|
|
8
|
+
* - `ok`: a politica existe e veio. Segue a sessao.
|
|
9
|
+
* - `sem-politica`: o projeto existe e nunca foi briefado. NAO e falha: a saida e rodar o
|
|
10
|
+
* briefing. Tratar como erro faria todo projeto novo parecer quebrado.
|
|
11
|
+
* - `inalcancavel`: rede, timeout, 5xx, credencial, projeto inexistente. A politica pode
|
|
12
|
+
* existir e nao chegou — e ai a sessao precisa PARAR, porque seguir sem ela e
|
|
13
|
+
* exatamente a falha que este projeto combate.
|
|
14
|
+
*
|
|
15
|
+
* A diferenca entre as duas ultimas e o que o `GET /api/v1/artefatos` responde: `404` e
|
|
16
|
+
* "nao ha projeto"; `200` com `politica` nula e "existe, nunca briefado".
|
|
17
|
+
*/
|
|
18
|
+
export type ResultadoDaPolitica = {
|
|
19
|
+
estado: "ok";
|
|
20
|
+
conteudo: string;
|
|
21
|
+
} | {
|
|
22
|
+
estado: "sem-politica";
|
|
23
|
+
} | {
|
|
24
|
+
estado: "inalcancavel";
|
|
25
|
+
motivo: string;
|
|
26
|
+
};
|
|
27
|
+
export declare function buscaPolitica(raiz: string): Promise<ResultadoDaPolitica>;
|
package/dist/politica.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { cabecalhos, credencial, pede } from "./api.js";
|
|
2
|
+
export async function buscaPolitica(raiz) {
|
|
3
|
+
let config;
|
|
4
|
+
try {
|
|
5
|
+
config = await credencial(raiz);
|
|
6
|
+
}
|
|
7
|
+
catch (erro) {
|
|
8
|
+
// Sem `.dd-harness.json` ou sem token nao da para nem perguntar. E inalcancavel, nao
|
|
9
|
+
// "sem politica": a politica pode muito bem existir do outro lado.
|
|
10
|
+
return { estado: "inalcancavel", motivo: mensagem(erro) };
|
|
11
|
+
}
|
|
12
|
+
const url = new URL(`${config.config.api}/api/v1/artefatos`);
|
|
13
|
+
url.searchParams.set("tenant", config.config.tenant);
|
|
14
|
+
url.searchParams.set("projeto", config.config.projeto);
|
|
15
|
+
try {
|
|
16
|
+
const resposta = await pede(url, { headers: cabecalhos(config.token) });
|
|
17
|
+
if (!resposta.ok) {
|
|
18
|
+
const { erro } = (await resposta.json().catch(() => ({})));
|
|
19
|
+
return {
|
|
20
|
+
estado: "inalcancavel",
|
|
21
|
+
motivo: erro ?? `a API respondeu ${resposta.status}.`,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const payload = (await resposta.json());
|
|
25
|
+
const conteudo = payload.politica?.trim();
|
|
26
|
+
// Vazio e nulo sao a mesma coisa aqui, e os dois significam "nunca foi escrita".
|
|
27
|
+
if (!conteudo)
|
|
28
|
+
return { estado: "sem-politica" };
|
|
29
|
+
return { estado: "ok", conteudo };
|
|
30
|
+
}
|
|
31
|
+
catch (erro) {
|
|
32
|
+
return { estado: "inalcancavel", motivo: mensagem(erro) };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const mensagem = (erro) => erro instanceof Error ? erro.message : String(erro);
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dd-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
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",
|
|
7
7
|
"author": "Diego Dias",
|
|
8
8
|
"keywords": [
|