dd-harness 0.2.0 → 0.3.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
+ }
package/dist/check.d.ts CHANGED
@@ -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/diff.d.ts CHANGED
@@ -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,9 +1,11 @@
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
6
  import { init, SUGESTAO_MCP } from "./init.js";
6
7
  import { LINHA_DE_IMPORT } from "./materializa.js";
8
+ import { buscaPolitica } from "./politica.js";
7
9
  import { busca } from "./buscar.js";
8
10
  import { arquiva, edita } from "./curar.js";
9
11
  import { criaPasta } from "./pasta.js";
@@ -17,29 +19,32 @@ import { sync } from "./sync.js";
17
19
  * em repositorio alheio tem que envelhecer bem, e cada dependencia e uma chance de nao
18
20
  * envelhecer.
19
21
  */
20
- const AJUDA = `dd-harness — materializa política, briefing e Brain no repositório
21
-
22
- dd-harness login --token <token> [--api <url>]
23
- guarda a credencial desta máquina
24
- dd-harness projeto <slug> --nome "<nome>" [--tenant <t>] [--api <url>]
25
- cria o projeto no serviço (antes do init)
26
- dd-harness init --tenant <t> --projeto <p> [--api <url>]
27
- prepara o repositório (config + CLAUDE.md)
28
- dd-harness pasta <slug> --definicao "o que entra e o que não entra"
29
- cria a pasta que o gravar exige
30
- dd-harness gravar <arquivo.md> registra uma memória a partir de um markdown
31
- dd-harness editar <arquivo.md> corrige o que já está gravado
32
- dd-harness arquivar <pasta>/<slug> --motivo <obsoleta|incorreta|fora_dos_filtros>
33
- [--substituida-por <pasta>/<slug>]
34
- tira de circulação sem apagar
35
- dd-harness buscar "<pergunta>" acha memória por relevância, não por arquivo
36
- dd-harness sync escreve os artefatos em disco
37
- dd-harness check [--commit <sha>] mede as âncoras e reporta a deriva
38
- dd-harness status só lê: o tamanho do Brain e o que espera julgamento
39
- dd-harness --help
40
-
41
- O gerado vive em dd-harness/. Na raiz fica só o seu CLAUDE.md, que importa a
42
- política com a linha ${LINHA_DE_IMPORT}
22
+ const AJUDA = `dd-harness — materializa política, briefing e Brain no repositório
23
+
24
+ dd-harness login --token <token> [--api <url>]
25
+ guarda a credencial desta máquina
26
+ dd-harness projeto <slug> --nome "<nome>" [--tenant <t>] [--api <url>]
27
+ cria o projeto no serviço (antes do init)
28
+ dd-harness init --tenant <t> --projeto <p> [--api <url>]
29
+ prepara o repositório (config + CLAUDE.md)
30
+ dd-harness pasta <slug> --definicao "o que entra e o que não entra"
31
+ cria a pasta que o gravar exige
32
+ dd-harness gravar <arquivo.md> registra uma memória a partir de um markdown
33
+ dd-harness editar <arquivo.md> corrige o que já está gravado
34
+ dd-harness arquivar <pasta>/<slug> --motivo <obsoleta|incorreta|fora_dos_filtros>
35
+ [--substituida-por <pasta>/<slug>]
36
+ tira de circulação sem apagar
37
+ dd-harness buscar "<pergunta>" acha memória por relevância, não por arquivo
38
+ dd-harness sync escreve os artefatos em disco
39
+ dd-harness check [--commit <sha>] mede as âncoras e reporta a deriva
40
+ dd-harness status só lê: o tamanho do Brain e o que espera julgamento
41
+ dd-harness politica imprime a política do serviço (para o hook de sessão)
42
+ saída 0 = veio; 3 = projeto sem política;
43
+ 1 = não consegui buscar
44
+ dd-harness --help
45
+
46
+ O gerado vive em dd-harness/. Na raiz fica só o seu CLAUDE.md, que importa a
47
+ política com a linha ${LINHA_DE_IMPORT}
43
48
  `;
44
49
  /** O aviso existe porque o import falha em silêncio — foi medido, não suposto. */
45
50
  function avisaSobreOPonteiro(ponteiro) {
@@ -76,17 +81,17 @@ function avisaSobreOPonteiro(ponteiro) {
76
81
  * nosso: escrever la dentro sem a pessoa pedir e o mesmo tipo de invasao que
77
82
  * sobrescrever o CLAUDE.md dela. Quem cola, decide.
78
83
  */
79
- const GANCHOS = `
80
- Opcional — dois ganchos que valem a pena:
81
-
82
- .git/hooks/post-commit (avisa quais memórias falam do que você mudou)
83
- #!/bin/sh
84
- dd-harness check --commit "$(git rev-parse HEAD)" || true
85
-
86
- .claude/settings.json (na abertura da sessão, o que espera julgamento)
87
- "hooks": { "SessionStart": [{ "hooks": [{ "type": "command",
88
- "command": "dd-harness status" }] }] }
89
-
84
+ const GANCHOS = `
85
+ Opcional — dois ganchos que valem a pena:
86
+
87
+ .git/hooks/post-commit (avisa quais memórias falam do que você mudou)
88
+ #!/bin/sh
89
+ dd-harness check --commit "$(git rev-parse HEAD)" || true
90
+
91
+ .claude/settings.json (na abertura da sessão, o que espera julgamento)
92
+ "hooks": { "SessionStart": [{ "hooks": [{ "type": "command",
93
+ "command": "dd-harness status" }] }] }
94
+
90
95
  Os dois terminam em sucesso mesmo com deriva: avisam, não bloqueiam.`;
91
96
  function argumento(argv, nome) {
92
97
  const i = argv.indexOf(`--${nome}`);
@@ -164,7 +169,7 @@ async function comandoArquivar(argv) {
164
169
  console.log(" foi para o histórico, não foi apagada. Rode `dd-harness sync`.");
165
170
  }
166
171
  async function comandoBuscar(argv) {
167
- const consulta = argv.filter((a) => !a.startsWith("--")).join(" ").trim();
172
+ const consulta = termosDaConsulta(argv);
168
173
  if (!consulta)
169
174
  throw new Error('uso: dd-harness buscar "<pergunta>"');
170
175
  const limite = Number(argumento(argv, "limite")) || undefined;
@@ -261,6 +266,27 @@ async function comandoCheck(argv) {
261
266
  }
262
267
  }
263
268
  }
269
+ /**
270
+ * Imprime a politica no stdout, para o hook `SessionStart` injetar no contexto.
271
+ *
272
+ * Os codigos de saida sao o contrato com o hook, e existem para separar duas coisas que
273
+ * um "falhou" generico colapsaria: projeto novo (que precisa de briefing) de politica
274
+ * inalcancavel (que precisa PARAR a sessao). Mudar estes numeros quebra o hook.
275
+ */
276
+ async function comandoPolitica() {
277
+ const r = await buscaPolitica(process.cwd());
278
+ if (r.estado === "ok") {
279
+ console.log(r.conteudo);
280
+ return;
281
+ }
282
+ if (r.estado === "sem-politica") {
283
+ console.error("Este projeto ainda não tem política — nunca foi briefado. Rode `/briefar`.");
284
+ process.exitCode = 3;
285
+ return;
286
+ }
287
+ console.error(`não consegui buscar a política: ${r.motivo}`);
288
+ process.exitCode = 1;
289
+ }
264
290
  async function comandoStatus() {
265
291
  const r = await status(process.cwd());
266
292
  // O acervo vem primeiro, e sempre: e a resposta para "o que ha no Brain deste projeto?",
@@ -276,6 +302,14 @@ async function comandoStatus() {
276
302
  (arquivadas ? ` e ${arquivadas} arquivada(s)` : "") +
277
303
  (detalhe ? ` — ${detalhe}` : ""));
278
304
  }
305
+ // Antes do early return abaixo: fila travada nao e "julgamento esperando", e sumiria
306
+ // justamente na sessao mais comum — a que nao tem deriva nem vencida.
307
+ if (r.travadasNaFila > 0) {
308
+ console.log("");
309
+ console.log(`AVISO: ${r.travadasNaFila} memória(s) desistiram de ser indexadas e não entram na busca semântica.`);
310
+ console.log(" O indexador tentou 5 vezes e parou. Rode `pnpm worker --reindexar`;");
311
+ console.log(" se repetir, o motivo está em `embedding_queue.ultimo_erro`.");
312
+ }
279
313
  if (!r.comDeriva.length && !r.vencidas.length) {
280
314
  console.log("Nada esperando julgamento.");
281
315
  return;
@@ -366,6 +400,8 @@ async function principal() {
366
400
  return comandoCheck(resto);
367
401
  case "status":
368
402
  return comandoStatus();
403
+ case "politica":
404
+ return comandoPolitica();
369
405
  case "--help":
370
406
  case "-h":
371
407
  case undefined:
package/dist/init.js CHANGED
@@ -10,12 +10,12 @@ import { LINHA_DE_IMPORT } from "./materializa.js";
10
10
  * acrescenta a linha ao que voce escreveu, sem tocar no resto. Adotar um projeto vira
11
11
  * uma linha, e nao um ritual de mover arquivo.
12
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.
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
19
  `;
20
20
  /**
21
21
  * O `.mcp.json` e sugerido, nunca escrito.
@@ -24,13 +24,13 @@ ela a sessão abre sem protocolo, e nada avisa.
24
24
  * outros servidores, e mesclar JSON alheio e onde falha silenciosa nasce — entrada errada
25
25
  * nao da erro, a ferramenta so nao aparece. Quem cola sabe o que colou.
26
26
  */
27
- export const SUGESTAO_MCP = `{
28
- "mcpServers": {
29
- "dd-harness": {
30
- "command": "npx",
31
- "args": ["-y", "dd-harness-mcp"]
32
- }
33
- }
27
+ export const SUGESTAO_MCP = `{
28
+ "mcpServers": {
29
+ "dd-harness": {
30
+ "command": "npx",
31
+ "args": ["-y", "dd-harness-mcp"]
32
+ }
33
+ }
34
34
  }`;
35
35
  async function declaraMcp(raiz) {
36
36
  try {
@@ -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,44 +1,44 @@
1
- {
2
- "name": "dd-harness",
3
- "version": "0.2.0",
4
- "type": "module",
5
- "description": "Materializa politica, briefing e Brain do dd-harness no repositorio. Sem dependencia: fetch, crypto e fs sao do Node.",
6
- "license": "UNLICENSED",
7
- "author": "Diego Dias",
8
- "keywords": [
9
- "claude-code",
10
- "ai-agents",
11
- "memory",
12
- "brain",
13
- "cli"
14
- ],
15
- "homepage": "https://dd-harness.vercel.app",
16
- "repository": {
17
- "type": "git",
18
- "url": "git+https://github.com/diegodias93/dd-harness-online.git",
19
- "directory": "packages/cli"
20
- },
21
- "bin": {
22
- "dd-harness": "dist/index.js"
23
- },
24
- "//exports": "Aponta para `dist` porque quem IMPORTA isto em tempo de execucao e o Node, que nao executa TypeScript. Nao aponte para `src`: o Node segue os imports relativos de dentro do arquivo e tenta abrir `./api.js` ao lado do `.ts`, que nao existe. Quem consome no workspace e o `packages/mcp`, e ele compila o fonte do CLI junto (ver o tsconfig.build.json dele) em vez de depender deste campo — assim o build funciona num checkout limpo, sem `dist` previo.",
25
- "exports": {
26
- "./api": "./dist/api.js",
27
- "./buscar": "./dist/buscar.js",
28
- "./curar": "./dist/curar.js",
29
- "./gravar": "./dist/gravar.js",
30
- "./pasta": "./dist/pasta.js",
31
- "./projeto": "./dist/projeto.js"
32
- },
33
- "files": [
34
- "dist"
35
- ],
36
- "engines": {
37
- "node": ">=20"
38
- },
39
- "scripts": {
40
- "typecheck": "tsc -p . --noEmit",
41
- "build": "tsc -p tsconfig.build.json",
42
- "prepublishOnly": "npm run build"
43
- }
44
- }
1
+ {
2
+ "name": "dd-harness",
3
+ "version": "0.3.0",
4
+ "type": "module",
5
+ "description": "Materializa politica, briefing e Brain do dd-harness no repositorio. Sem dependencia: fetch, crypto e fs sao do Node.",
6
+ "license": "UNLICENSED",
7
+ "author": "Diego Dias",
8
+ "keywords": [
9
+ "claude-code",
10
+ "ai-agents",
11
+ "memory",
12
+ "brain",
13
+ "cli"
14
+ ],
15
+ "homepage": "https://dd-harness.vercel.app",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/diegodias93/dd-harness-online.git",
19
+ "directory": "packages/cli"
20
+ },
21
+ "bin": {
22
+ "dd-harness": "dist/index.js"
23
+ },
24
+ "//exports": "Aponta para `dist` porque quem IMPORTA isto em tempo de execucao e o Node, que nao executa TypeScript. Nao aponte para `src`: o Node segue os imports relativos de dentro do arquivo e tenta abrir `./api.js` ao lado do `.ts`, que nao existe. Quem consome no workspace e o `packages/mcp`, e ele compila o fonte do CLI junto (ver o tsconfig.build.json dele) em vez de depender deste campo — assim o build funciona num checkout limpo, sem `dist` previo.",
25
+ "exports": {
26
+ "./api": "./dist/api.js",
27
+ "./buscar": "./dist/buscar.js",
28
+ "./curar": "./dist/curar.js",
29
+ "./gravar": "./dist/gravar.js",
30
+ "./pasta": "./dist/pasta.js",
31
+ "./projeto": "./dist/projeto.js"
32
+ },
33
+ "files": [
34
+ "dist"
35
+ ],
36
+ "engines": {
37
+ "node": ">=20"
38
+ },
39
+ "scripts": {
40
+ "typecheck": "tsc -p . --noEmit",
41
+ "build": "tsc -p tsconfig.build.json",
42
+ "prepublishOnly": "npm run build"
43
+ }
44
+ }