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 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
+ }>;
@@ -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
+ }
@@ -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 "./materializa.js";
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
- * Curadoria pelo agente: editar e arquivar.
5
+ * A memoria inteira, no markdown que `gravar` e `editar` consomem.
8
6
  *
9
- * `gravar` sabia criar e mais nada. Uma memoria errada ficava errada, porque corrigir
10
- * exigia abrir a interface e o `CLAUDE.md` trata curadoria como obrigacao ("se
11
- * encontrar uma memoria obsoleta ou errada, corrija").
12
- *
13
- * Editar reaproveita o mesmo markdown de `gravar`, de proposito: o agente edita o arquivo
14
- * que o `sync` materializou e manda de volta. Um formato so para as duas operacoes, e o
15
- * que ele ja sabe ler.
16
- */
17
- /**
18
- * Depois que o servico aceita, o arquivo em disco deixa de ser "edicao nao enviada" — e o
19
- * manifesto tem que saber, senao o `sync` seguinte para com "editado a mao" e a unica saida
20
- * que resta e descartar o arquivo. O ciclo materializa-corrige-envia travava justamente
21
- * aqui, com o servico ja atualizado.
22
- *
23
- * Guarda o hash do que foi enviado, nao do que o servico devolveria: o `sync` seguinte
24
- * reescreve o arquivo na forma canonica quando o ETag mudar.
7
+ * Sem materializacao, esta e a unica forma de ler o corpo: a busca devolve so endereco,
8
+ * titulo e resumo, e o disco nao tem mais nada. Tambem e o ponto de partida de qualquer
9
+ * edicao corrigir exige ver o que esta la.
25
10
  */
26
- async function marcaComoEnviado(raiz, caminho, conteudo) {
27
- const chave = relative(raiz, caminho).split("\\").join("/");
28
- const manifesto = await leManifesto(raiz);
29
- // Arquivo de fora do repositorio (um rascunho em /tmp, por exemplo) nao esta no
30
- // manifesto e nao deve entrar: o manifesto descreve o que o `sync` materializou.
31
- if (!manifesto.arquivos[chave])
32
- return;
33
- manifesto.arquivos[chave] = hashDe(conteudo);
34
- await gravaManifesto(raiz, manifesto);
11
+ export async function le(raiz, endereco) {
12
+ const { config, token } = await credencial(raiz);
13
+ const url = new URL(`${config.api}/api/v1/memorias/${endereco}`);
14
+ url.searchParams.set("tenant", config.tenant);
15
+ url.searchParams.set("projeto", config.projeto);
16
+ const resposta = await pede(url, { headers: cabecalhos(token) });
17
+ if (!resposta.ok)
18
+ await recusa(resposta);
19
+ return comoMarkdown((await resposta.json()));
20
+ }
21
+ /** O formato canonico: o mesmo que `interpreta` le, para o ciclo fechar sem conversao. */
22
+ export function comoMarkdown(m) {
23
+ const ancoras = m.ancoras.length
24
+ ? `\n## Âncoras\n\n${m.ancoras.map((a) => `- \`${a.valor}\``).join("\n")}\n`
25
+ : "";
26
+ return [
27
+ "---",
28
+ `name: ${m.slug}`,
29
+ `titulo: ${m.titulo}`,
30
+ `description: ${m.resumo}`,
31
+ `pasta: ${m.pasta}`,
32
+ ...(m.revisar_ate ? [`revisar-ate: ${m.revisar_ate.slice(0, 10)}`] : []),
33
+ "---",
34
+ "",
35
+ m.corpo.trim(),
36
+ "",
37
+ "## Os três filtros",
38
+ "",
39
+ `**Dano:** ${m.dano}`,
40
+ "",
41
+ `**Invisibilidade:** ${m.invisibilidade}`,
42
+ "",
43
+ `**Externalidade:** ${m.externalidade}`,
44
+ ancoras,
45
+ ].join("\n");
35
46
  }
36
47
  export async function edita(raiz, caminho) {
37
48
  const { config, token } = await credencial(raiz);
@@ -55,7 +66,6 @@ export async function edita(raiz, caminho) {
55
66
  });
56
67
  if (!resposta.ok)
57
68
  await recusa(resposta);
58
- await marcaComoEnviado(raiz, caminho, cru);
59
69
  return { endereco, ancoras: memoria.ancoras.length };
60
70
  }
61
71
  export async function arquiva(raiz, endereco, opcoes) {
package/dist/diff.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Brain } from "./materializa.js";
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 casa = caminhos.some((c) => c === ancora.valor || c.startsWith(`${ancora.valor}/`));
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 corte = resto.search(/\n##\s+Os tr[êe]s filtros\s*\n/i);
50
- if (corte === -1)
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 { LINHA_DE_IMPORT } from "./materializa.js";
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 — materializa política, briefing e Brain no repositório
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 sync escreve os artefatos em disco
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
- O gerado vive em dd-harness/. Na raiz fica o seu CLAUDE.md, que importa a
42
- política com a linha ${LINHA_DE_IMPORT}
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 — dois ganchos que valem a pena:
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
- .claude/settings.json (na abertura da sessão, o que espera julgamento)
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
- Os dois terminam em sucesso mesmo com deriva: avisam, não bloqueiam.`;
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
- criado: "criado CLAUDE.md com a linha de import",
109
- "linha-acrescentada": "ajustado CLAUDE.md linha de import acrescentada ao seu",
110
- "ja-tinha-a-linha": "mantido CLAUDE.md — já importava a política",
111
- }[r.claudeMd]);
112
- console.log("\nAgora: dd-harness login --token <token> && dd-harness sync");
81
+ console.log("\nAgora: dd-harness login --token <token>");
82
+ // O hook vem primeiro e nao e opcional: sem ele a sessao abre sem politica, que e a
83
+ // falha que este projeto existe para combater. O MCP e conveniencia; este, nao.
84
+ if (r.hook === "ja-declarado") {
85
+ console.log("\nmantido .claude/settings.json — o hook da política já está declarado");
86
+ }
87
+ else {
88
+ console.log("\nOBRIGATÓRIO: o hook que carrega a política no início de cada sessão." +
89
+ "\nSem ele a sessão abre sem protocolo, e nada avisa. Acrescente ao" +
90
+ "\n`.claude/settings.json` (não escrevo nele: o arquivo é seu e pode já" +
91
+ "\nter hooks e permissões):\n");
92
+ console.log(SUGESTAO_HOOK);
93
+ }
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). Rode \`dd-harness sync\` para materializar.`);
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. Rode `dd-harness sync`.");
145
+ console.log(" foi para o histórico, não foi apagada.");
146
+ }
147
+ /**
148
+ * A memoria inteira no stdout, no mesmo markdown que `gravar` e `editar` consomem.
149
+ *
150
+ * Sem materializacao este e o unico caminho para o corpo: a busca devolve so endereco,
151
+ * titulo e resumo. Tambem e o ponto de partida de toda edicao — corrigir exige ver.
152
+ */
153
+ async function comandoLer(argv) {
154
+ const endereco = argv[0];
155
+ if (!endereco || endereco.startsWith("-")) {
156
+ throw new Error("uso: dd-harness ler <pasta>/<slug>");
157
+ }
158
+ console.log(await le(process.cwd(), endereco));
165
159
  }
166
160
  async function comandoBuscar(argv) {
167
- const consulta = argv.filter((a) => !a.startsWith("--")).join(" ").trim();
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 nao fica no repositorio: quem
331
- * materializa e o `sync`, a partir do servico, para nao existir copia escrita a mao ao
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
- * Pelo mesmo motivo do `CLAUDE.md` da raiz: o arquivo e do repositorio, pode ja declarar
10
- * outros servidores, e mesclar JSON alheio e onde falha silenciosa nasce entrada errada
11
- * nao da erro, a ferramenta so nao aparece. Quem cola sabe o que colou.
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
- * Pelo mesmo motivo do `CLAUDE.md` da raiz: o arquivo e do repositorio, pode ja declarar
24
- * outros servidores, e mesclar JSON alheio e onde falha silenciosa nasce entrada errada
25
- * nao da erro, a ferramenta so nao aparece. Quem cola sabe o que colou.
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
- const caminhoClaude = join(raiz, "CLAUDE.md");
62
- let claudeMd;
63
- let atual = null;
64
- try {
65
- atual = await readFile(caminhoClaude, "utf8");
66
- }
67
- catch {
68
- atual = null;
69
- }
70
- if (atual === null) {
71
- await writeFile(caminhoClaude, `${CABECALHO}\n${LINHA_DE_IMPORT}\n`, "utf8");
72
- claudeMd = "criado";
73
- }
74
- else if (atual.includes(LINHA_DE_IMPORT)) {
75
- claudeMd = "ja-tinha-a-linha";
76
- }
77
- else {
78
- // Acrescenta no fim, sem reescrever nada do que ja estava la.
79
- const separador = atual.endsWith("\n") ? "\n" : "\n\n";
80
- await writeFile(caminhoClaude, `${atual}${separador}${LINHA_DE_IMPORT}\n`, "utf8");
81
- claudeMd = "linha-acrescentada";
82
- }
83
- return { config, claudeMd, mcp: await declaraMcp(raiz) };
90
+ return {
91
+ config,
92
+ mcp: await declaraMcp(raiz),
93
+ hook: await declaraHook(raiz),
94
+ };
84
95
  }
@@ -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>;
@@ -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.2.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
- "description": "Materializa politica, briefing e Brain do dd-harness no repositorio. Sem dependencia: fetch, crypto e fs sao do Node.",
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": [