dd-harness 0.1.0 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +11 -11
- package/README.md +64 -64
- package/dist/buscar.js +21 -0
- package/dist/check.js +2 -1
- package/dist/curar.js +66 -0
- package/dist/gravar.js +111 -0
- package/dist/index.js +142 -24
- package/dist/init.js +6 -6
- package/dist/materializa.js +12 -5
- package/dist/pasta.js +36 -0
- package/dist/sync.js +8 -5
- package/package.json +11 -3
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/buscar.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { leConfigDoRepo, leToken } from "./config.js";
|
|
2
|
+
export async function busca(raiz, consulta, limite) {
|
|
3
|
+
const config = await leConfigDoRepo(raiz);
|
|
4
|
+
const token = await leToken(config.api);
|
|
5
|
+
if (!token) {
|
|
6
|
+
throw new Error(`sem credencial para ${config.api}. Rode \`dd-harness login --token <token>\`.`);
|
|
7
|
+
}
|
|
8
|
+
const url = new URL(`${config.api}/api/v1/busca`);
|
|
9
|
+
url.searchParams.set("tenant", config.tenant);
|
|
10
|
+
url.searchParams.set("projeto", config.projeto);
|
|
11
|
+
url.searchParams.set("q", consulta);
|
|
12
|
+
if (limite)
|
|
13
|
+
url.searchParams.set("limite", String(limite));
|
|
14
|
+
const resposta = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
15
|
+
if (!resposta.ok) {
|
|
16
|
+
const { erro } = (await resposta.json().catch(() => ({})));
|
|
17
|
+
throw new Error(erro ?? `a API respondeu ${resposta.status}.`);
|
|
18
|
+
}
|
|
19
|
+
const lido = (await resposta.json());
|
|
20
|
+
return { semantica: lido.semantica, provedor: lido.provedor, achados: lido.resultados };
|
|
21
|
+
}
|
package/dist/check.js
CHANGED
|
@@ -72,7 +72,7 @@ export async function check(raiz, commit) {
|
|
|
72
72
|
? memoriasTocadas(brain, await caminhosDoCommit(raiz, commit))
|
|
73
73
|
: [];
|
|
74
74
|
if (medicoes.length === 0) {
|
|
75
|
-
return { medidas: 0, ausentes: [], novas: 0, base: 0, jaAbertas: 0, tocadas };
|
|
75
|
+
return { medidas: 0, ausentes: [], novas: 0, base: 0, jaAbertas: 0, fechadas: 0, tocadas };
|
|
76
76
|
}
|
|
77
77
|
const envio = await fetch(`${config.api}/api/v1/deriva`, {
|
|
78
78
|
method: "POST",
|
|
@@ -97,6 +97,7 @@ export async function check(raiz, commit) {
|
|
|
97
97
|
novas: julgamento.novas,
|
|
98
98
|
base: julgamento.base,
|
|
99
99
|
jaAbertas: julgamento.ja_abertas,
|
|
100
|
+
fechadas: julgamento.fechadas ?? 0,
|
|
100
101
|
tocadas,
|
|
101
102
|
};
|
|
102
103
|
}
|
package/dist/curar.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { leConfigDoRepo, leToken } from "./config.js";
|
|
3
|
+
import { interpreta } from "./gravar.js";
|
|
4
|
+
/**
|
|
5
|
+
* Curadoria pelo agente: editar e arquivar.
|
|
6
|
+
*
|
|
7
|
+
* `gravar` sabia criar e mais nada. Uma memoria errada ficava errada, porque corrigir
|
|
8
|
+
* exigia abrir a interface — e o `CLAUDE.md` trata curadoria como obrigacao ("se
|
|
9
|
+
* encontrar uma memoria obsoleta ou errada, corrija").
|
|
10
|
+
*
|
|
11
|
+
* Editar reaproveita o mesmo markdown de `gravar`, de proposito: o agente edita o arquivo
|
|
12
|
+
* que o `sync` materializou e manda de volta. Um formato so para as duas operacoes, e o
|
|
13
|
+
* que ele ja sabe ler.
|
|
14
|
+
*/
|
|
15
|
+
async function credencial(raiz) {
|
|
16
|
+
const config = await leConfigDoRepo(raiz);
|
|
17
|
+
const token = await leToken(config.api);
|
|
18
|
+
if (!token) {
|
|
19
|
+
throw new Error(`sem credencial para ${config.api}. Rode \`dd-harness login --token <token>\`.`);
|
|
20
|
+
}
|
|
21
|
+
return { config, token };
|
|
22
|
+
}
|
|
23
|
+
async function recusa(resposta) {
|
|
24
|
+
const { erro } = (await resposta.json().catch(() => ({})));
|
|
25
|
+
throw new Error(erro ?? `a API respondeu ${resposta.status}.`);
|
|
26
|
+
}
|
|
27
|
+
export async function edita(raiz, caminho) {
|
|
28
|
+
const { config, token } = await credencial(raiz);
|
|
29
|
+
const memoria = interpreta(await readFile(caminho, "utf8"));
|
|
30
|
+
const endereco = `${memoria.pasta}/${memoria.slug}`;
|
|
31
|
+
const resposta = await fetch(`${config.api}/api/v1/memorias/${endereco}`, {
|
|
32
|
+
method: "PATCH",
|
|
33
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
34
|
+
body: JSON.stringify({
|
|
35
|
+
tenant: config.tenant,
|
|
36
|
+
projeto: config.projeto,
|
|
37
|
+
titulo: memoria.titulo,
|
|
38
|
+
resumo: memoria.resumo,
|
|
39
|
+
corpo: memoria.corpo,
|
|
40
|
+
dano: memoria.dano,
|
|
41
|
+
invisibilidade: memoria.invisibilidade,
|
|
42
|
+
externalidade: memoria.externalidade,
|
|
43
|
+
ancoras: memoria.ancoras,
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
if (!resposta.ok)
|
|
47
|
+
await recusa(resposta);
|
|
48
|
+
return { endereco, ancoras: memoria.ancoras.length };
|
|
49
|
+
}
|
|
50
|
+
export async function arquiva(raiz, endereco, opcoes) {
|
|
51
|
+
const { config, token } = await credencial(raiz);
|
|
52
|
+
const resposta = await fetch(`${config.api}/api/v1/memorias/${endereco}`, {
|
|
53
|
+
method: "DELETE",
|
|
54
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
55
|
+
body: JSON.stringify({
|
|
56
|
+
tenant: config.tenant,
|
|
57
|
+
projeto: config.projeto,
|
|
58
|
+
motivo: opcoes.motivo,
|
|
59
|
+
...(opcoes.substituidaPor ? { substituida_por: opcoes.substituidaPor } : {}),
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
if (!resposta.ok)
|
|
63
|
+
await recusa(resposta);
|
|
64
|
+
const lido = (await resposta.json());
|
|
65
|
+
return lido;
|
|
66
|
+
}
|
package/dist/gravar.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { leConfigDoRepo, leToken } from "./config.js";
|
|
3
|
+
const OBRIGATORIOS = ["name", "titulo", "description", "pasta"];
|
|
4
|
+
/** Frontmatter simples: `chave: valor` por linha. Sem lib de YAML — nao ha aninhamento. */
|
|
5
|
+
function leFrontmatter(texto) {
|
|
6
|
+
const linhas = texto.replace(/\r\n/g, "\n").split("\n");
|
|
7
|
+
if (linhas[0]?.trim() !== "---") {
|
|
8
|
+
throw new Error("o arquivo precisa começar com `---` e um frontmatter.");
|
|
9
|
+
}
|
|
10
|
+
const fim = linhas.indexOf("---", 1);
|
|
11
|
+
if (fim === -1)
|
|
12
|
+
throw new Error("frontmatter sem `---` de fechamento.");
|
|
13
|
+
const campos = new Map();
|
|
14
|
+
for (const linha of linhas.slice(1, fim)) {
|
|
15
|
+
const corte = linha.indexOf(":");
|
|
16
|
+
if (corte === -1)
|
|
17
|
+
continue;
|
|
18
|
+
campos.set(linha.slice(0, corte).trim(), linha.slice(corte + 1).trim());
|
|
19
|
+
}
|
|
20
|
+
return { campos, resto: linhas.slice(fim + 1).join("\n") };
|
|
21
|
+
}
|
|
22
|
+
/** `**Dano:** texto` — devolve o texto, sem o rotulo. */
|
|
23
|
+
function filtro(corpo, rotulo) {
|
|
24
|
+
const achado = corpo.match(new RegExp(`\\*\\*${rotulo}:\\*\\*\\s*([\\s\\S]*?)(?=\\n\\s*\\n\\*\\*|\\n## |$)`, "i"));
|
|
25
|
+
const valor = achado?.[1]?.trim() ?? "";
|
|
26
|
+
if (!valor)
|
|
27
|
+
throw new Error(`falta o filtro **${rotulo}:** no arquivo.`);
|
|
28
|
+
return valor;
|
|
29
|
+
}
|
|
30
|
+
/** Itens de lista da secao de ancoras, com ou sem crase em volta. */
|
|
31
|
+
function ancorasDe(texto) {
|
|
32
|
+
const secao = texto.split(/\n##\s+[ÂA]ncoras\s*\n/i)[1];
|
|
33
|
+
if (!secao)
|
|
34
|
+
return [];
|
|
35
|
+
return secao
|
|
36
|
+
.split("\n")
|
|
37
|
+
.map((l) => l.match(/^\s*[-*]\s+(.+?)\s*$/)?.[1])
|
|
38
|
+
.filter((v) => Boolean(v))
|
|
39
|
+
.map((v) => v.replace(/^`|`$/g, "").trim())
|
|
40
|
+
.filter(Boolean);
|
|
41
|
+
}
|
|
42
|
+
export function interpreta(texto) {
|
|
43
|
+
const { campos, resto } = leFrontmatter(texto);
|
|
44
|
+
for (const chave of OBRIGATORIOS) {
|
|
45
|
+
if (!campos.get(chave))
|
|
46
|
+
throw new Error(`frontmatter sem \`${chave}\`.`);
|
|
47
|
+
}
|
|
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)
|
|
51
|
+
throw new Error("falta a seção `## Os três filtros`.");
|
|
52
|
+
const corpo = resto
|
|
53
|
+
.slice(0, corte)
|
|
54
|
+
.replace(/<!--[\s\S]*?-->/g, "")
|
|
55
|
+
.trim();
|
|
56
|
+
if (!corpo)
|
|
57
|
+
throw new Error("o corpo da memória está vazio.");
|
|
58
|
+
const cauda = resto.slice(corte);
|
|
59
|
+
const tambem = campos.get("projetos") ?? "";
|
|
60
|
+
return {
|
|
61
|
+
slug: campos.get("name"),
|
|
62
|
+
titulo: campos.get("titulo"),
|
|
63
|
+
resumo: campos.get("description"),
|
|
64
|
+
pasta: campos.get("pasta"),
|
|
65
|
+
corpo,
|
|
66
|
+
dano: filtro(cauda, "Dano"),
|
|
67
|
+
invisibilidade: filtro(cauda, "Invisibilidade"),
|
|
68
|
+
externalidade: filtro(cauda, "Externalidade"),
|
|
69
|
+
ancoras: ancorasDe(cauda),
|
|
70
|
+
tambemEm: tambem
|
|
71
|
+
.split(",")
|
|
72
|
+
.map((s) => s.trim())
|
|
73
|
+
.filter(Boolean),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export async function grava(raiz, caminho) {
|
|
77
|
+
const config = await leConfigDoRepo(raiz);
|
|
78
|
+
const token = await leToken(config.api);
|
|
79
|
+
if (!token) {
|
|
80
|
+
throw new Error(`sem credencial para ${config.api}. Rode \`dd-harness login --token <token>\`.`);
|
|
81
|
+
}
|
|
82
|
+
const memoria = interpreta(await readFile(caminho, "utf8"));
|
|
83
|
+
const resposta = await fetch(`${config.api}/api/v1/memorias`, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
86
|
+
body: JSON.stringify({
|
|
87
|
+
tenant: config.tenant,
|
|
88
|
+
projeto: config.projeto,
|
|
89
|
+
pasta: memoria.pasta,
|
|
90
|
+
slug: memoria.slug,
|
|
91
|
+
titulo: memoria.titulo,
|
|
92
|
+
resumo: memoria.resumo,
|
|
93
|
+
corpo: memoria.corpo,
|
|
94
|
+
dano: memoria.dano,
|
|
95
|
+
invisibilidade: memoria.invisibilidade,
|
|
96
|
+
externalidade: memoria.externalidade,
|
|
97
|
+
ancoras: memoria.ancoras,
|
|
98
|
+
tambem_em: memoria.tambemEm,
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
if (!resposta.ok) {
|
|
102
|
+
const { erro } = (await resposta.json().catch(() => ({})));
|
|
103
|
+
throw new Error(erro ?? `a API respondeu ${resposta.status}.`);
|
|
104
|
+
}
|
|
105
|
+
const { endereco } = (await resposta.json());
|
|
106
|
+
return {
|
|
107
|
+
endereco,
|
|
108
|
+
ancoras: memoria.ancoras.length,
|
|
109
|
+
projetos: 1 + memoria.tambemEm.length,
|
|
110
|
+
};
|
|
111
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { check, status } from "./check.js";
|
|
3
3
|
import { leConfigDoRepo, guardaToken } from "./config.js";
|
|
4
|
+
import { grava } from "./gravar.js";
|
|
4
5
|
import { init } from "./init.js";
|
|
5
6
|
import { LINHA_DE_IMPORT } from "./materializa.js";
|
|
7
|
+
import { busca } from "./buscar.js";
|
|
8
|
+
import { arquiva, edita } from "./curar.js";
|
|
9
|
+
import { criaPasta } from "./pasta.js";
|
|
6
10
|
import { sync } from "./sync.js";
|
|
7
11
|
/**
|
|
8
12
|
* `dd-harness` — o cliente que materializa os artefatos no repositorio.
|
|
@@ -12,23 +16,44 @@ import { sync } from "./sync.js";
|
|
|
12
16
|
* em repositorio alheio tem que envelhecer bem, e cada dependencia e uma chance de nao
|
|
13
17
|
* envelhecer.
|
|
14
18
|
*/
|
|
15
|
-
const AJUDA = `dd-harness — materializa política, briefing e Brain no repositório
|
|
16
|
-
|
|
17
|
-
dd-harness init --tenant <t> --projeto <p> [--api <url>]
|
|
18
|
-
prepara o repositório (config + CLAUDE.md)
|
|
19
|
-
dd-harness login --token <token> guarda a credencial desta máquina
|
|
20
|
-
dd-harness
|
|
21
|
-
|
|
22
|
-
dd-harness
|
|
23
|
-
dd-harness
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
const AJUDA = `dd-harness — materializa política, briefing e Brain no repositório
|
|
20
|
+
|
|
21
|
+
dd-harness init --tenant <t> --projeto <p> [--api <url>]
|
|
22
|
+
prepara o repositório (config + CLAUDE.md)
|
|
23
|
+
dd-harness login --token <token> guarda a credencial desta máquina
|
|
24
|
+
dd-harness pasta <slug> --definicao "o que entra e o que não entra"
|
|
25
|
+
cria a pasta que o gravar exige
|
|
26
|
+
dd-harness gravar <arquivo.md> registra uma memória a partir de um markdown
|
|
27
|
+
dd-harness editar <arquivo.md> corrige o que já está gravado
|
|
28
|
+
dd-harness arquivar <pasta>/<slug> --motivo <obsoleta|incorreta|fora_dos_filtros>
|
|
29
|
+
[--substituida-por <pasta>/<slug>]
|
|
30
|
+
tira de circulação sem apagar
|
|
31
|
+
dd-harness buscar "<pergunta>" acha memória por relevância, não por arquivo
|
|
32
|
+
dd-harness sync escreve os artefatos em disco
|
|
33
|
+
dd-harness check [--commit <sha>] mede as âncoras e reporta a deriva
|
|
34
|
+
dd-harness status só lê: o que espera julgamento
|
|
35
|
+
dd-harness --help
|
|
36
|
+
|
|
37
|
+
O gerado vive em dd-harness/. Na raiz fica só o seu CLAUDE.md, que importa a
|
|
38
|
+
política com a linha ${LINHA_DE_IMPORT}
|
|
27
39
|
`;
|
|
28
40
|
/** O aviso existe porque o import falha em silêncio — foi medido, não suposto. */
|
|
29
41
|
function avisaSobreOPonteiro(ponteiro) {
|
|
30
42
|
if (ponteiro === "ok" || ponteiro === "sem-politica")
|
|
31
43
|
return;
|
|
44
|
+
// O import pendurado e o inverso dos outros dois: a linha esta la, o alvo e que nao
|
|
45
|
+
// existe. Dizer "acrescente a linha" aqui mandaria a pessoa para o lugar errado.
|
|
46
|
+
if (ponteiro === "aponta-para-o-vazio") {
|
|
47
|
+
console.error([
|
|
48
|
+
"",
|
|
49
|
+
`AVISO: o CLAUDE.md importa ${LINHA_DE_IMPORT}, mas não há política no serviço.`,
|
|
50
|
+
"O arquivo apontado não existe, e import quebrado falha em silêncio: a sessão abre",
|
|
51
|
+
"sem protocolo e nada avisa.",
|
|
52
|
+
"",
|
|
53
|
+
"Escreva a política do projeto no serviço, ou tire a linha do CLAUDE.md.",
|
|
54
|
+
].join("\n"));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
32
57
|
const motivo = ponteiro === "sem-claude-md"
|
|
33
58
|
? "não há CLAUDE.md na raiz"
|
|
34
59
|
: "o CLAUDE.md da raiz não importa a política";
|
|
@@ -47,17 +72,17 @@ function avisaSobreOPonteiro(ponteiro) {
|
|
|
47
72
|
* nosso: escrever la dentro sem a pessoa pedir e o mesmo tipo de invasao que
|
|
48
73
|
* sobrescrever o CLAUDE.md dela. Quem cola, decide.
|
|
49
74
|
*/
|
|
50
|
-
const GANCHOS = `
|
|
51
|
-
Opcional — dois ganchos que valem a pena:
|
|
52
|
-
|
|
53
|
-
.git/hooks/post-commit (avisa quais memórias falam do que você mudou)
|
|
54
|
-
#!/bin/sh
|
|
55
|
-
dd-harness check --commit "$(git rev-parse HEAD)" || true
|
|
56
|
-
|
|
57
|
-
.claude/settings.json (na abertura da sessão, o que espera julgamento)
|
|
58
|
-
"hooks": { "SessionStart": [{ "hooks": [{ "type": "command",
|
|
59
|
-
"command": "dd-harness status" }] }] }
|
|
60
|
-
|
|
75
|
+
const GANCHOS = `
|
|
76
|
+
Opcional — dois ganchos que valem a pena:
|
|
77
|
+
|
|
78
|
+
.git/hooks/post-commit (avisa quais memórias falam do que você mudou)
|
|
79
|
+
#!/bin/sh
|
|
80
|
+
dd-harness check --commit "$(git rev-parse HEAD)" || true
|
|
81
|
+
|
|
82
|
+
.claude/settings.json (na abertura da sessão, o que espera julgamento)
|
|
83
|
+
"hooks": { "SessionStart": [{ "hooks": [{ "type": "command",
|
|
84
|
+
"command": "dd-harness status" }] }] }
|
|
85
|
+
|
|
61
86
|
Os dois terminam em sucesso mesmo com deriva: avisam, não bloqueiam.`;
|
|
62
87
|
function argumento(argv, nome) {
|
|
63
88
|
const i = argv.indexOf(`--${nome}`);
|
|
@@ -90,6 +115,57 @@ async function comandoLogin(argv) {
|
|
|
90
115
|
const caminho = await guardaToken(config.api, token);
|
|
91
116
|
console.log(`credencial de ${config.api} guardada em ${caminho}`);
|
|
92
117
|
}
|
|
118
|
+
async function comandoEditar(argv) {
|
|
119
|
+
const caminho = argv[0];
|
|
120
|
+
if (!caminho || caminho.startsWith("-")) {
|
|
121
|
+
throw new Error("uso: dd-harness editar <arquivo.md>");
|
|
122
|
+
}
|
|
123
|
+
const r = await edita(process.cwd(), caminho);
|
|
124
|
+
console.log(`editado ${r.endereco}`);
|
|
125
|
+
console.log(` ${r.ancoras} âncora(s). Rode \`dd-harness sync\` para materializar.`);
|
|
126
|
+
}
|
|
127
|
+
const MOTIVOS = ["obsoleta", "incorreta", "fora_dos_filtros"];
|
|
128
|
+
async function comandoArquivar(argv) {
|
|
129
|
+
const endereco = argv[0];
|
|
130
|
+
const motivo = argumento(argv, "motivo");
|
|
131
|
+
if (!endereco || endereco.startsWith("-") || !endereco.includes("/")) {
|
|
132
|
+
throw new Error("uso: dd-harness arquivar <pasta>/<slug> --motivo <obsoleta|incorreta|fora_dos_filtros>");
|
|
133
|
+
}
|
|
134
|
+
// Lista fechada tambem no cliente: errar o motivo aqui devolve a lista, em vez de um
|
|
135
|
+
// 400 do servidor com a mesma informacao mais longe de quem digitou.
|
|
136
|
+
if (!motivo || !MOTIVOS.includes(motivo)) {
|
|
137
|
+
throw new Error(`--motivo precisa ser um de: ${MOTIVOS.join(", ")}`);
|
|
138
|
+
}
|
|
139
|
+
const r = await arquiva(process.cwd(), endereco, {
|
|
140
|
+
motivo: motivo,
|
|
141
|
+
substituidaPor: argumento(argv, "substituida-por"),
|
|
142
|
+
});
|
|
143
|
+
console.log(`arquivado ${r.endereco} (${r.motivo})`);
|
|
144
|
+
console.log(" foi para o histórico, não foi apagada. Rode `dd-harness sync`.");
|
|
145
|
+
}
|
|
146
|
+
async function comandoBuscar(argv) {
|
|
147
|
+
const consulta = argv.filter((a) => !a.startsWith("--")).join(" ").trim();
|
|
148
|
+
if (!consulta)
|
|
149
|
+
throw new Error('uso: dd-harness buscar "<pergunta>"');
|
|
150
|
+
const limite = Number(argumento(argv, "limite")) || undefined;
|
|
151
|
+
const r = await busca(process.cwd(), consulta, limite);
|
|
152
|
+
if (!r.achados.length) {
|
|
153
|
+
console.log("nada encontrado.");
|
|
154
|
+
// Sem esta linha, "nada encontrado" vira "nao existe" — quando pode ser so a
|
|
155
|
+
// semantica desligada, e a memoria estar la com outras palavras.
|
|
156
|
+
if (!r.semantica) {
|
|
157
|
+
console.log(" (só busca textual: sem provedor de embedding no serviço)");
|
|
158
|
+
}
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
console.log(r.semantica
|
|
162
|
+
? `${r.achados.length} resultado(s) — busca híbrida (${r.provedor}):`
|
|
163
|
+
: `${r.achados.length} resultado(s) — só textual, sem semântica:`);
|
|
164
|
+
for (const a of r.achados) {
|
|
165
|
+
console.log(` ${a.endereco} — ${a.titulo}`);
|
|
166
|
+
console.log(` ${a.resumo}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
93
169
|
async function comandoSync() {
|
|
94
170
|
const resultado = await sync(process.cwd());
|
|
95
171
|
if (resultado.tipo === "editado-a-mao") {
|
|
@@ -142,7 +218,12 @@ async function comandoCheck(argv) {
|
|
|
142
218
|
if (r.jaAbertas > 0) {
|
|
143
219
|
console.log(`${r.jaAbertas} já estava(m) aberta(s) — nada novo, só o carimbo.`);
|
|
144
220
|
}
|
|
145
|
-
|
|
221
|
+
// Reverter uma mudanca fecha a observacao que ela abriu. Anunciar isso importa: sem a
|
|
222
|
+
// linha, a fila encolhe sem explicacao e quem olha o `status` acha que perdeu algo.
|
|
223
|
+
if (r.fechadas > 0) {
|
|
224
|
+
console.log(`${r.fechadas} deriva(s) fechada(s): o alvo voltou a bater com a linha de base.`);
|
|
225
|
+
}
|
|
226
|
+
if (!r.ausentes.length && r.novas === 0 && r.jaAbertas === 0 && r.fechadas === 0) {
|
|
146
227
|
console.log("nenhuma deriva: o mundo ainda bate com o que as memórias dizem.");
|
|
147
228
|
}
|
|
148
229
|
// O cruzamento com o diff e outra coisa que deriva: e "voce acabou de mexer no que
|
|
@@ -177,6 +258,33 @@ async function comandoStatus() {
|
|
|
177
258
|
console.log(` ${m.pasta}/${m.memoria} — ${m.titulo}`);
|
|
178
259
|
}
|
|
179
260
|
}
|
|
261
|
+
async function comandoPasta(argv) {
|
|
262
|
+
const slug = argv[0];
|
|
263
|
+
const definicao = argumento(argv, "definicao");
|
|
264
|
+
if (!slug || slug.startsWith("-") || !definicao) {
|
|
265
|
+
throw new Error('uso: dd-harness pasta <slug> --definicao "o que entra e o que não entra"');
|
|
266
|
+
}
|
|
267
|
+
const r = await criaPasta(process.cwd(), slug, definicao);
|
|
268
|
+
console.log(r.jaExistia
|
|
269
|
+
? `pasta ${r.pasta} já existia — nada criado.`
|
|
270
|
+
: `criada pasta ${r.pasta}. Agora \`dd-harness gravar <arquivo.md>\` aceita \`pasta: ${r.pasta}\`.`);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* O agente escreve o arquivo — que e o que ele ja fazia no modelo file-based — e este
|
|
274
|
+
* comando o transforma em requisicao. O arquivo nao fica no repositorio: quem
|
|
275
|
+
* materializa e o `sync`, a partir do servico, para nao existir copia escrita a mao ao
|
|
276
|
+
* lado da copia gerada.
|
|
277
|
+
*/
|
|
278
|
+
async function comandoGravar(argv) {
|
|
279
|
+
const caminho = argv[0];
|
|
280
|
+
if (!caminho || caminho.startsWith("-")) {
|
|
281
|
+
throw new Error("uso: dd-harness gravar <arquivo.md>");
|
|
282
|
+
}
|
|
283
|
+
const r = await grava(process.cwd(), caminho);
|
|
284
|
+
console.log(`gravado ${r.endereco}`);
|
|
285
|
+
console.log(` ${r.ancoras} âncora(s), valendo para ${r.projetos} projeto(s).` +
|
|
286
|
+
" Rode `dd-harness sync` para materializar.");
|
|
287
|
+
}
|
|
180
288
|
async function principal() {
|
|
181
289
|
const [comando, ...resto] = process.argv.slice(2);
|
|
182
290
|
switch (comando) {
|
|
@@ -184,6 +292,16 @@ async function principal() {
|
|
|
184
292
|
return comandoInit(resto);
|
|
185
293
|
case "login":
|
|
186
294
|
return comandoLogin(resto);
|
|
295
|
+
case "pasta":
|
|
296
|
+
return comandoPasta(resto);
|
|
297
|
+
case "gravar":
|
|
298
|
+
return comandoGravar(resto);
|
|
299
|
+
case "editar":
|
|
300
|
+
return comandoEditar(resto);
|
|
301
|
+
case "arquivar":
|
|
302
|
+
return comandoArquivar(resto);
|
|
303
|
+
case "buscar":
|
|
304
|
+
return comandoBuscar(resto);
|
|
187
305
|
case "sync":
|
|
188
306
|
return comandoSync();
|
|
189
307
|
case "check":
|
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
|
export async function init(raiz, dados) {
|
|
21
21
|
const caminhoConfig = join(raiz, CAMINHO_CONFIG);
|
package/dist/materializa.js
CHANGED
|
@@ -54,12 +54,19 @@ export function arquivoDoIndice(brain) {
|
|
|
54
54
|
const ativas = brain.memorias.filter((m) => m.status === "ativa");
|
|
55
55
|
const historico = brain.memorias.filter((m) => m.status === "historico");
|
|
56
56
|
const linha = (m) => `- [${m.titulo}](${m.pasta}/${m.slug}.md) — ${m.resumo.replace(/\n/g, " ")}`;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
// O indice e montado pelas pastas, mas quem manda e a memoria: pasta que so aparece no
|
|
58
|
+
// `pasta:` de uma memoria tambem vira secao. Sem isso a memoria fica em disco e fora do
|
|
59
|
+
// indice — arquivo presente que nenhum agente encontra, que e a mesma falha silenciosa
|
|
60
|
+
// do import quebrado.
|
|
61
|
+
const definicoes = new Map(brain.pastas.map((p) => [p.slug, p.definicao]));
|
|
62
|
+
const slugs = [...new Set([...definicoes.keys(), ...ativas.map((m) => m.pasta)])].sort();
|
|
63
|
+
const secoes = slugs
|
|
64
|
+
.map((slug) => {
|
|
65
|
+
const memorias = ativas.filter((m) => m.pasta === slug);
|
|
66
|
+
const definicao = definicoes.get(slug);
|
|
60
67
|
return [
|
|
61
|
-
`## ${
|
|
62
|
-
`<!-- ${
|
|
68
|
+
`## ${slug}`,
|
|
69
|
+
`<!-- ${definicao ?? "(definição não veio do serviço)"} -->`,
|
|
63
70
|
...(memorias.length ? memorias.map(linha) : ["<!-- (vazia) -->"]),
|
|
64
71
|
].join("\n");
|
|
65
72
|
})
|
package/dist/pasta.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { leConfigDoRepo, leToken } from "./config.js";
|
|
2
|
+
/**
|
|
3
|
+
* `dd-harness pasta <slug> --definicao "..."` — cria a pasta sem sair da sessao.
|
|
4
|
+
*
|
|
5
|
+
* `gravar` recusa quando a pasta nao existe, e essa recusa e boa: e ela que impede um
|
|
6
|
+
* typo no `pasta:` de virar pasta nova em silencio. O que faltava era a saida — ate aqui
|
|
7
|
+
* o agente parava e pedia que alguem abrisse o navegador.
|
|
8
|
+
*/
|
|
9
|
+
export async function criaPasta(raiz, slug, definicao) {
|
|
10
|
+
const config = await leConfigDoRepo(raiz);
|
|
11
|
+
const token = await leToken(config.api);
|
|
12
|
+
if (!token) {
|
|
13
|
+
throw new Error(`sem credencial para ${config.api}. Rode \`dd-harness login --token <token>\`.`);
|
|
14
|
+
}
|
|
15
|
+
const resposta = await fetch(`${config.api}/api/v1/pastas`, {
|
|
16
|
+
method: "POST",
|
|
17
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
18
|
+
body: JSON.stringify({
|
|
19
|
+
tenant: config.tenant,
|
|
20
|
+
projeto: config.projeto,
|
|
21
|
+
slug,
|
|
22
|
+
definicao,
|
|
23
|
+
}),
|
|
24
|
+
});
|
|
25
|
+
// Pasta que ja existe nao e falha para quem chamou: o estado desejado ja vale. Vale
|
|
26
|
+
// dizer que ja existia — quem pediu pode estar corrigindo um typo e precisa saber que
|
|
27
|
+
// nao criou nada.
|
|
28
|
+
if (resposta.status === 409)
|
|
29
|
+
return { pasta: slug, jaExistia: true };
|
|
30
|
+
if (!resposta.ok) {
|
|
31
|
+
const { erro } = (await resposta.json().catch(() => ({})));
|
|
32
|
+
throw new Error(erro ?? `a API respondeu ${resposta.status}.`);
|
|
33
|
+
}
|
|
34
|
+
const { pasta } = (await resposta.json());
|
|
35
|
+
return { pasta, jaExistia: false };
|
|
36
|
+
}
|
package/dist/sync.js
CHANGED
|
@@ -57,16 +57,19 @@ async function buscaBrain(config, token, etag) {
|
|
|
57
57
|
/**
|
|
58
58
|
* O `CLAUDE.md` da raiz existe e importa a politica?
|
|
59
59
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
60
|
+
* Sem politica no servico E sem a linha, nao ha o que apontar nem o que avisar — avisar
|
|
61
|
+
* ai seria ruido que ensina a ignorar aviso. Mas com a linha escrita e a politica vazia o
|
|
62
|
+
* import esta pendurado de verdade, e esse e o silencio que a memoria
|
|
63
|
+
* `import-do-claude-md-falha-calado` manda quebrar.
|
|
62
64
|
*/
|
|
63
65
|
export async function confereOPonteiro(raiz, temPolitica) {
|
|
64
|
-
if (!temPolitica)
|
|
65
|
-
return "sem-politica";
|
|
66
66
|
const claudeMd = await leSeExistir(join(raiz, "CLAUDE.md"));
|
|
67
|
+
const temALinha = claudeMd?.includes(LINHA_DE_IMPORT) ?? false;
|
|
68
|
+
if (!temPolitica)
|
|
69
|
+
return temALinha ? "aponta-para-o-vazio" : "sem-politica";
|
|
67
70
|
if (claudeMd === null)
|
|
68
71
|
return "sem-claude-md";
|
|
69
|
-
return
|
|
72
|
+
return temALinha ? "ok" : "sem-a-linha";
|
|
70
73
|
}
|
|
71
74
|
export async function sync(raiz) {
|
|
72
75
|
const config = await leConfigDoRepo(raiz);
|
package/package.json
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dd-harness",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Materializa politica, briefing e Brain do dd-harness no repositorio. Sem dependencia: fetch, crypto e fs sao do Node.",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"author": "Diego Dias",
|
|
8
|
-
"keywords": [
|
|
8
|
+
"keywords": [
|
|
9
|
+
"claude-code",
|
|
10
|
+
"ai-agents",
|
|
11
|
+
"memory",
|
|
12
|
+
"brain",
|
|
13
|
+
"cli"
|
|
14
|
+
],
|
|
9
15
|
"homepage": "https://dd-harness.vercel.app",
|
|
10
16
|
"repository": {
|
|
11
17
|
"type": "git",
|
|
@@ -15,7 +21,9 @@
|
|
|
15
21
|
"bin": {
|
|
16
22
|
"dd-harness": "dist/index.js"
|
|
17
23
|
},
|
|
18
|
-
"files": [
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
19
27
|
"engines": {
|
|
20
28
|
"node": ">=20"
|
|
21
29
|
},
|