dd-harness 0.3.0 → 0.5.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/dist/init.js CHANGED
@@ -1,37 +1,72 @@
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
4
  /**
6
- * Prepara um repositorio para o dd-harness.
5
+ * O `.mcp.json` e sugerido, nunca escrito.
7
6
  *
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.
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.
12
10
  */
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
- `;
11
+ export const SUGESTAO_MCP = `{
12
+ "mcpServers": {
13
+ "dd-harness": {
14
+ "command": "npx",
15
+ "args": ["-y", "dd-harness-mcp"]
16
+ }
17
+ }
18
+ }`;
20
19
  /**
21
- * O `.mcp.json` e sugerido, nunca escrito.
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.
22
26
  *
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.
27
+ * Sugerido e nao escrito, pelo mesmo motivo do `.mcp.json`.
26
28
  */
27
- export const SUGESTAO_MCP = `{
28
- "mcpServers": {
29
- "dd-harness": {
30
- "command": "npx",
31
- "args": ["-y", "dd-harness-mcp"]
32
- }
33
- }
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
+ }
34
43
  }`;
44
+ /**
45
+ * O que escrever num `AGENTS.md`, para agente que NAO e o Claude Code.
46
+ *
47
+ * O hook de `SessionStart` e um mecanismo do Claude Code: Codex, Cursor, Gemini CLI e
48
+ * Windsurf nao o executam. Nessas ferramentas nada carrega a politica sozinho — e o
49
+ * resultado e a mesma falha de sempre, a sessao abrindo sem protocolo sem avisar.
50
+ *
51
+ * O que resta la e o servidor MCP, que essas ferramentas suportam. Mas ferramenta
52
+ * disponivel nao e ferramenta chamada: sem uma instrucao explicita, o agente pode
53
+ * simplesmente nunca perguntar pela politica. Dai esta linha, que e curta de proposito —
54
+ * ela manda buscar a regra, nao repete a regra.
55
+ */
56
+ export const SUGESTAO_AGENTS = `# AGENTS.md
57
+
58
+ Este projeto tem um protocolo de trabalho que **não está neste arquivo**.
59
+
60
+ **ANTES de qualquer outra coisa**, chame a ferramenta MCP \`ler_artefato\` com
61
+ \`tipo: "politica"\` e siga o que ela devolver. É a primeira ação da sessão, antes
62
+ de ler código, responder ou planejar.
63
+
64
+ - Se a ferramenta não existir, o servidor MCP do dd-harness não está declarado:
65
+ avise o usuário e **não modifique nada** até ele resolver.
66
+ - Se ela devolver vazio, este projeto nunca foi briefado — diga isso ao usuário.
67
+
68
+ O Claude Code carrega a política sozinho, por hook. Nas outras ferramentas, a
69
+ chamada acima é o que substitui esse hook.`;
35
70
  async function declaraMcp(raiz) {
36
71
  try {
37
72
  const cru = await readFile(join(raiz, ".mcp.json"), "utf8");
@@ -43,6 +78,26 @@ async function declaraMcp(raiz) {
43
78
  return "a-declarar";
44
79
  }
45
80
  }
81
+ /**
82
+ * O hook ja esta declarado?
83
+ *
84
+ * Procura pelo COMANDO, nao pela forma: `settings.json` aceita varios formatos de
85
+ * matcher, e quem ja tem o hook pode te-lo escrito de outro jeito. O que importa e se
86
+ * `dd-harness politica` roda no inicio da sessao.
87
+ */
88
+ async function declaraHook(raiz) {
89
+ for (const arquivo of [".claude/settings.json", ".claude/settings.local.json"]) {
90
+ try {
91
+ const cru = await readFile(join(raiz, arquivo), "utf8");
92
+ if (cru.includes("dd-harness politica"))
93
+ return "ja-declarado";
94
+ }
95
+ catch {
96
+ // Sem arquivo: segue para o proximo.
97
+ }
98
+ }
99
+ return "a-declarar";
100
+ }
46
101
  export async function init(raiz, dados) {
47
102
  const caminhoConfig = join(raiz, CAMINHO_CONFIG);
48
103
  let config = "ja-existia";
@@ -58,27 +113,26 @@ export async function init(raiz, dados) {
58
113
  await writeFile(caminhoConfig, `${JSON.stringify(conteudo, null, 2)}\n`, "utf8");
59
114
  config = "criada";
60
115
  }
61
- const caminhoClaude = join(raiz, "CLAUDE.md");
62
- let claudeMd;
63
- let atual = null;
116
+ return {
117
+ config,
118
+ mcp: await declaraMcp(raiz),
119
+ hook: await declaraHook(raiz),
120
+ agents: await apontaNoAgents(raiz),
121
+ };
122
+ }
123
+ /**
124
+ * O `AGENTS.md` ja manda buscar a politica?
125
+ *
126
+ * Procura pela CHAMADA (`ler_artefato`), nao por uma frase exata: quem ja escreveu a
127
+ * instrucao pode te-la redigido de outro jeito, e sugerir de novo por causa de palavra
128
+ * diferente e ruido.
129
+ */
130
+ async function apontaNoAgents(raiz) {
64
131
  try {
65
- atual = await readFile(caminhoClaude, "utf8");
132
+ const cru = await readFile(join(raiz, "AGENTS.md"), "utf8");
133
+ return cru.includes("ler_artefato") ? "ja-aponta" : "a-apontar";
66
134
  }
67
135
  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";
136
+ return "a-apontar";
82
137
  }
83
- return { config, claudeMd, mcp: await declaraMcp(raiz) };
84
138
  }
@@ -15,7 +15,7 @@
15
15
  * A diferenca entre as duas ultimas e o que o `GET /api/v1/artefatos` responde: `404` e
16
16
  * "nao ha projeto"; `200` com `politica` nula e "existe, nunca briefado".
17
17
  */
18
- export type ResultadoDaPolitica = {
18
+ export type ResultadoDaPolitica = ({
19
19
  estado: "ok";
20
20
  conteudo: string;
21
21
  } | {
@@ -23,5 +23,11 @@ export type ResultadoDaPolitica = {
23
23
  } | {
24
24
  estado: "inalcancavel";
25
25
  motivo: string;
26
+ }) & {
27
+ /**
28
+ * Memorias esperando o worker. Vem de carona no mesmo payload da politica — o hook ja
29
+ * faz esta chamada, entao saber o estado da fila custa zero requisicao.
30
+ */
31
+ esperandoIndexacao?: number;
26
32
  };
27
33
  export declare function buscaPolitica(raiz: string): Promise<ResultadoDaPolitica>;
package/dist/politica.js CHANGED
@@ -23,10 +23,11 @@ export async function buscaPolitica(raiz) {
23
23
  }
24
24
  const payload = (await resposta.json());
25
25
  const conteudo = payload.politica?.trim();
26
+ const esperandoIndexacao = payload.esperando_indexacao ?? 0;
26
27
  // Vazio e nulo sao a mesma coisa aqui, e os dois significam "nunca foi escrita".
27
28
  if (!conteudo)
28
- return { estado: "sem-politica" };
29
- return { estado: "ok", conteudo };
29
+ return { estado: "sem-politica", esperandoIndexacao };
30
+ return { estado: "ok", conteudo, esperandoIndexacao };
30
31
  }
31
32
  catch (erro) {
32
33
  return { estado: "inalcancavel", motivo: mensagem(erro) };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Trocar o alvo de uma ancora, sem reescrever a memoria.
3
+ *
4
+ * O caso: uma refatoracao move um arquivo, e toda memoria ancorada nele passa a acusar
5
+ * "alvo ausente". O `check --commit` cruza isso com os renames do git e diz para onde o
6
+ * conteudo foi; este comando aplica a troca.
7
+ *
8
+ * Separado de `editar` de proposito: editar exige o markdown inteiro e mexe no conteudo,
9
+ * que nao e o que mudou aqui. Reancorar troca um endereco e mais nada — misturar as duas
10
+ * coisas convidaria a reescrever o corpo de memoria enquanto se conserta um caminho.
11
+ */
12
+ export declare function reancora(raiz: string, endereco: string, de: string, para: string): Promise<{
13
+ endereco: string;
14
+ de: string;
15
+ para: string;
16
+ }>;
@@ -0,0 +1,48 @@
1
+ import { cabecalhos, credencial, pede, recusa } from "./api.js";
2
+ import {} from "./curar.js";
3
+ /**
4
+ * Trocar o alvo de uma ancora, sem reescrever a memoria.
5
+ *
6
+ * O caso: uma refatoracao move um arquivo, e toda memoria ancorada nele passa a acusar
7
+ * "alvo ausente". O `check --commit` cruza isso com os renames do git e diz para onde o
8
+ * conteudo foi; este comando aplica a troca.
9
+ *
10
+ * Separado de `editar` de proposito: editar exige o markdown inteiro e mexe no conteudo,
11
+ * que nao e o que mudou aqui. Reancorar troca um endereco e mais nada — misturar as duas
12
+ * coisas convidaria a reescrever o corpo de memoria enquanto se conserta um caminho.
13
+ */
14
+ export async function reancora(raiz, endereco, de, para) {
15
+ const { config, token } = await credencial(raiz);
16
+ const url = new URL(`${config.api}/api/v1/memorias/${endereco}`);
17
+ url.searchParams.set("tenant", config.tenant);
18
+ url.searchParams.set("projeto", config.projeto);
19
+ const leitura = await pede(url, { headers: cabecalhos(token) });
20
+ if (!leitura.ok)
21
+ await recusa(leitura);
22
+ const memoria = (await leitura.json());
23
+ if (!memoria.ancoras.some((a) => a.valor === de)) {
24
+ throw new Error(`${endereco} não tem âncora em "${de}". Âncoras atuais: ` +
25
+ (memoria.ancoras.map((a) => a.valor).join(", ") || "nenhuma"));
26
+ }
27
+ const novas = memoria.ancoras.map((a) => (a.valor === de ? { ...a, valor: para } : a));
28
+ // Reaproveita o PATCH de edicao: ele recebe a memoria inteira, entao mandamos o que ja
29
+ // estava la com a ancora trocada. Uma porta so para escrever memoria, e nao duas.
30
+ const envio = await pede(`${config.api}/api/v1/memorias/${endereco}`, {
31
+ method: "PATCH",
32
+ headers: cabecalhos(token, true),
33
+ body: JSON.stringify({
34
+ tenant: config.tenant,
35
+ projeto: config.projeto,
36
+ titulo: memoria.titulo,
37
+ resumo: memoria.resumo,
38
+ corpo: memoria.corpo,
39
+ dano: memoria.dano,
40
+ invisibilidade: memoria.invisibilidade,
41
+ externalidade: memoria.externalidade,
42
+ ancoras: novas.map((a) => a.valor),
43
+ }),
44
+ });
45
+ if (!envio.ok)
46
+ await recusa(envio);
47
+ return { endereco, de, para };
48
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Subir o worker de embeddings quando ha fila esperando.
3
+ *
4
+ * O worker nao esta hospedado (decisao de custo, registrada no ROADMAP): roda local, a
5
+ * mao. O problema disso e a degradacao SILENCIOSA — a memoria e gravada, a busca responde
6
+ * `semantica: true` porque a CONSULTA foi vetorizada, e mesmo assim nao acha nada, porque
7
+ * a BASE ainda nao tem vetor. Quem procura conclui "nao existe" quando o certo era
8
+ * "ainda nao indexei".
9
+ *
10
+ * Por isso o hook de sessao sobe o worker sozinho. Duas travas, e as duas importam:
11
+ *
12
+ * - **So sobe quando ha fila.** O hook roda em TODA sessao, inclusive nas que so leem
13
+ * codigo. Subir com a fila vazia gastaria chamada de embedding sem ninguem ter pedido.
14
+ * - **So no repositorio que TEM o worker.** Ele vive neste monorepo, nao no repositorio
15
+ * consumidor: la o `pnpm worker` nao existe, e tentar rodar daria erro a cada sessao.
16
+ */
17
+ /** O worker vive aqui dentro. Noutro repositorio, nao ha o que subir. */
18
+ export declare function temWorkerLocal(raiz: string): Promise<boolean>;
19
+ export type Subida = {
20
+ subiu: true;
21
+ } | {
22
+ subiu: false;
23
+ motivo: "sem-fila" | "sem-worker" | "falhou";
24
+ detalhe?: string;
25
+ };
26
+ /**
27
+ * Dispara um lote e devolve na hora — nao espera terminar.
28
+ *
29
+ * `--uma-vez` e nao o modo continuo: o hook de sessao nao pode deixar processo de pe que
30
+ * ninguem mandou subir, e um lote basta para o caso comum (as memorias gravadas na sessao
31
+ * anterior). Fila grande volta a aparecer na proxima sessao, e ai o numero cresce em vez
32
+ * de sumir — que e o sinal de que esta na hora de hospedar de verdade.
33
+ *
34
+ * `unref()` solta o processo do pai: sem isso o hook so retornaria quando o worker
35
+ * terminasse, e a sessao ficaria esperando embedding para abrir.
36
+ */
37
+ export declare function subiuOWorker(raiz: string, esperando: number): Promise<Subida>;
package/dist/worker.js ADDED
@@ -0,0 +1,65 @@
1
+ import { spawn } from "node:child_process";
2
+ import { access } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ /**
5
+ * Subir o worker de embeddings quando ha fila esperando.
6
+ *
7
+ * O worker nao esta hospedado (decisao de custo, registrada no ROADMAP): roda local, a
8
+ * mao. O problema disso e a degradacao SILENCIOSA — a memoria e gravada, a busca responde
9
+ * `semantica: true` porque a CONSULTA foi vetorizada, e mesmo assim nao acha nada, porque
10
+ * a BASE ainda nao tem vetor. Quem procura conclui "nao existe" quando o certo era
11
+ * "ainda nao indexei".
12
+ *
13
+ * Por isso o hook de sessao sobe o worker sozinho. Duas travas, e as duas importam:
14
+ *
15
+ * - **So sobe quando ha fila.** O hook roda em TODA sessao, inclusive nas que so leem
16
+ * codigo. Subir com a fila vazia gastaria chamada de embedding sem ninguem ter pedido.
17
+ * - **So no repositorio que TEM o worker.** Ele vive neste monorepo, nao no repositorio
18
+ * consumidor: la o `pnpm worker` nao existe, e tentar rodar daria erro a cada sessao.
19
+ */
20
+ /** O worker vive aqui dentro. Noutro repositorio, nao ha o que subir. */
21
+ export async function temWorkerLocal(raiz) {
22
+ try {
23
+ await access(join(raiz, "src", "worker", "indexador.ts"));
24
+ return true;
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ /**
31
+ * Dispara um lote e devolve na hora — nao espera terminar.
32
+ *
33
+ * `--uma-vez` e nao o modo continuo: o hook de sessao nao pode deixar processo de pe que
34
+ * ninguem mandou subir, e um lote basta para o caso comum (as memorias gravadas na sessao
35
+ * anterior). Fila grande volta a aparecer na proxima sessao, e ai o numero cresce em vez
36
+ * de sumir — que e o sinal de que esta na hora de hospedar de verdade.
37
+ *
38
+ * `unref()` solta o processo do pai: sem isso o hook so retornaria quando o worker
39
+ * terminasse, e a sessao ficaria esperando embedding para abrir.
40
+ */
41
+ export async function subiuOWorker(raiz, esperando) {
42
+ if (esperando <= 0)
43
+ return { subiu: false, motivo: "sem-fila" };
44
+ if (!(await temWorkerLocal(raiz))) {
45
+ return { subiu: false, motivo: "sem-worker" };
46
+ }
47
+ try {
48
+ const filho = spawn("npx", ["pnpm@latest", "worker", "--uma-vez"], {
49
+ cwd: raiz,
50
+ detached: true,
51
+ stdio: "ignore",
52
+ shell: process.platform === "win32",
53
+ windowsHide: true,
54
+ });
55
+ filho.unref();
56
+ return { subiu: true };
57
+ }
58
+ catch (erro) {
59
+ return {
60
+ subiu: false,
61
+ motivo: "falhou",
62
+ detalhe: erro instanceof Error ? erro.message : String(erro),
63
+ };
64
+ }
65
+ }
package/package.json CHANGED
@@ -1,44 +1,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
- }
1
+ {
2
+ "name": "dd-harness",
3
+ "version": "0.5.0",
4
+ "type": "module",
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
+ "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
+ }