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 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 sync escreve os artefatos em disco
21
- dd-harness check [--commit <sha>] mede as âncoras e reporta a deriva
22
- dd-harness status só lê: o que espera julgamento
23
- dd-harness --help
24
-
25
- O gerado vive em dd-harness/. Na raiz fica só o seu CLAUDE.md, que importa a
26
- política com a linha ${LINHA_DE_IMPORT}
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
- if (!r.ausentes.length && r.novas === 0 && r.jaAbertas === 0) {
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);
@@ -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
- const secoes = brain.pastas
58
- .map((pasta) => {
59
- const memorias = ativas.filter((m) => m.pasta === pasta.slug);
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
- `## ${pasta.slug}`,
62
- `<!-- ${pasta.definicao} -->`,
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
- * Nao ha politica no servico? Entao nao ha o que apontar, e a ausencia do ponteiro nao e
61
- * problema — avisar aqui seria ruido que ensina a ignorar aviso.
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 claudeMd.includes(LINHA_DE_IMPORT) ? "ok" : "sem-a-linha";
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.0",
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": ["claude-code", "ai-agents", "memory", "brain", "cli"],
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": ["dist"],
24
+ "files": [
25
+ "dist"
26
+ ],
19
27
  "engines": {
20
28
  "node": ">=20"
21
29
  },