dd-harness 0.1.1 → 0.2.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/api.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { type ConfigDoRepo } from "./config.js";
2
+ /**
3
+ * A fronteira com o servico: ler credencial e falar HTTP.
4
+ *
5
+ * Cada comando reimplementava as mesmas tres linhas — le config, le token, monta `fetch`
6
+ * com `Authorization` — e cada copia era uma chance de divergir na mensagem de erro.
7
+ * Aqui elas existem uma vez, e o servidor MCP consome as mesmas funcoes que o CLI: as
8
+ * duas portas falam com a API pelo mesmo caminho, entao corrigir uma corrige a outra.
9
+ */
10
+ export type Credencial = {
11
+ config: ConfigDoRepo;
12
+ token: string;
13
+ };
14
+ export declare function credencial(raiz: string): Promise<Credencial>;
15
+ /**
16
+ * O `erro` que a API devolve no corpo e mais util que o status, e e o que o agente le.
17
+ * Sem corpo interpretavel, o status ao menos diz que houve resposta.
18
+ */
19
+ export declare function recusa(resposta: Response): Promise<never>;
20
+ export declare function cabecalhos(token: string, comCorpo?: boolean): Record<string, string>;
21
+ export declare function pede(url: string | URL, init?: RequestInit, tentativas?: number): Promise<Response>;
package/dist/api.js ADDED
@@ -0,0 +1,69 @@
1
+ import { leConfigDoRepo, leToken } from "./config.js";
2
+ export async function credencial(raiz) {
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
+ return { config, token };
9
+ }
10
+ /**
11
+ * O `erro` que a API devolve no corpo e mais util que o status, e e o que o agente le.
12
+ * Sem corpo interpretavel, o status ao menos diz que houve resposta.
13
+ */
14
+ export async function recusa(resposta) {
15
+ const { erro } = (await resposta.json().catch(() => ({})));
16
+ throw new Error(erro ?? `a API respondeu ${resposta.status}.`);
17
+ }
18
+ export function cabecalhos(token, comCorpo = false) {
19
+ return comCorpo
20
+ ? { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }
21
+ : { Authorization: `Bearer ${token}` };
22
+ }
23
+ /**
24
+ * `fetch` com nova tentativa para falha transitoria.
25
+ *
26
+ * Medido em uso real: um `ECONNRESET` de conexao ociosa derrubou um `arquivar` que
27
+ * funcionou na segunda tentativa manual. Sem retry, falha de rede de um segundo vira erro
28
+ * final — e para um agente isso e a diferenca entre seguir sozinho e parar para pedir
29
+ * ajuda. Com varios agentes concorrendo, transitorio deixa de ser raro.
30
+ *
31
+ * O que NAO e repetido, e o cuidado que importa:
32
+ *
33
+ * - Erro de aplicacao (4xx). Token invalido ou memoria fora dos filtros nao melhora na
34
+ * segunda tentativa; repetir so atrasa a mensagem que o agente precisa ler.
35
+ * - 5xx em requisicao que ESCREVE. `POST /memorias` pode ter gravado antes de a resposta
36
+ * se perder, e repetir criaria duas. Perder a resposta de uma escrita que funcionou e
37
+ * ruim; gravar duas vezes e pior.
38
+ *
39
+ * Entao: repete falha de REDE (o `fetch` nem chegou a receber resposta) sempre, e 5xx
40
+ * apenas quando o metodo e seguro de repetir.
41
+ */
42
+ const IDEMPOTENTES = new Set(["GET", "HEAD", "PUT", "DELETE"]);
43
+ const espera = (ms) => new Promise((r) => setTimeout(r, ms));
44
+ export async function pede(url, init = {}, tentativas = 3) {
45
+ const metodo = (init.method ?? "GET").toUpperCase();
46
+ const podeRepetirErroDoServidor = IDEMPOTENTES.has(metodo);
47
+ let ultimoErro;
48
+ for (let tentativa = 1; tentativa <= tentativas; tentativa += 1) {
49
+ try {
50
+ const resposta = await fetch(url, init);
51
+ if (resposta.status >= 500 && podeRepetirErroDoServidor && tentativa < tentativas) {
52
+ await espera(tentativa * 400);
53
+ continue;
54
+ }
55
+ return resposta;
56
+ }
57
+ catch (erro) {
58
+ // Aqui o `fetch` falhou antes de qualquer resposta: DNS, conexao recusada,
59
+ // ECONNRESET. Nada foi processado do outro lado, entao repetir e seguro mesmo
60
+ // para POST.
61
+ ultimoErro = erro;
62
+ if (tentativa === tentativas)
63
+ break;
64
+ await espera(tentativa * 400);
65
+ }
66
+ }
67
+ const detalhe = ultimoErro instanceof Error ? ultimoErro.message : String(ultimoErro);
68
+ throw new Error(`não consegui falar com o serviço depois de ${tentativas} tentativas: ${detalhe}`);
69
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `dd-harness buscar "<pergunta>"` — recuperacao por relevancia.
3
+ *
4
+ * O contraponto da ancora: ela e exata e responde "que memorias falam deste arquivo?";
5
+ * esta responde "o que ja aprendemos sobre isto?", que e a pergunta de quem ainda esta
6
+ * entendendo o problema e nem sabe qual arquivo abrir.
7
+ *
8
+ * Devolve endereco, nao conteudo: quem quiser o corpo abre o arquivo que o `sync` ja
9
+ * materializou. Assim a busca continua barata e o Brain em disco continua sendo a fonte.
10
+ */
11
+ export type Achado = {
12
+ endereco: string;
13
+ titulo: string;
14
+ resumo: string;
15
+ };
16
+ export declare function busca(raiz: string, consulta: string, limite?: number): Promise<{
17
+ semantica: boolean;
18
+ provedor: string | null;
19
+ achados: Achado[];
20
+ }>;
package/dist/buscar.js ADDED
@@ -0,0 +1,15 @@
1
+ import { cabecalhos, credencial, pede, recusa } from "./api.js";
2
+ export async function busca(raiz, consulta, limite) {
3
+ const { config, token } = await credencial(raiz);
4
+ const url = new URL(`${config.api}/api/v1/busca`);
5
+ url.searchParams.set("tenant", config.tenant);
6
+ url.searchParams.set("projeto", config.projeto);
7
+ url.searchParams.set("q", consulta);
8
+ if (limite)
9
+ url.searchParams.set("limite", String(limite));
10
+ const resposta = await pede(url, { headers: cabecalhos(token) });
11
+ if (!resposta.ok)
12
+ await recusa(resposta);
13
+ const lido = (await resposta.json());
14
+ return { semantica: lido.semantica, provedor: lido.provedor, achados: lido.resultados };
15
+ }
@@ -0,0 +1,63 @@
1
+ import { type Tocada } from "./diff.js";
2
+ import type { Brain } from "./materializa.js";
3
+ /**
4
+ * `dd-harness check` — resolve as âncoras contra a árvore de trabalho.
5
+ *
6
+ * Mede cada alvo e manda o resultado ao serviço, que julga. Nunca falha o processo por
7
+ * encontrar deriva: sai com 0 e imprime. Deriva avisa, não bloqueia — bloqueio antes de
8
+ * o mecanismo ter reputação ensina o reflexo do `--no-verify`.
9
+ */
10
+ export type Medicao = {
11
+ pasta: string;
12
+ memoria: string;
13
+ valor: string;
14
+ sha: string | null;
15
+ detalhe?: string;
16
+ };
17
+ export type ResultadoDoCheck = {
18
+ medidas: number;
19
+ ausentes: string[];
20
+ novas: number;
21
+ base: number;
22
+ jaAbertas: number;
23
+ /** Observacoes que fecharam sozinhas: o alvo voltou a bater com a linha de base. */
24
+ fechadas: number;
25
+ /** Memorias que falam do que o commit mudou. Vazio quando nao ha `--commit`. */
26
+ tocadas: Tocada[];
27
+ };
28
+ export type ResultadoDoStatus = {
29
+ /**
30
+ * O tamanho do acervo, por pasta. Sem isto, um agente que pergunta "o que ha no Brain
31
+ * deste projeto?" nao tem comando que responda — e a saida que resta e contar arquivo
32
+ * em disco, que acerta por acidente: memoria gravada e nao sincronizada nao esta la, e
33
+ * memoria arquivada sai do disco mas continua no indice, em `## Historico`.
34
+ */
35
+ acervo: {
36
+ ativas: number;
37
+ arquivadas: number;
38
+ porPasta: {
39
+ pasta: string;
40
+ quantas: number;
41
+ }[];
42
+ };
43
+ comDeriva: {
44
+ pasta: string;
45
+ memoria: string;
46
+ titulo: string;
47
+ abertas: number;
48
+ }[];
49
+ vencidas: {
50
+ pasta: string;
51
+ memoria: string;
52
+ titulo: string;
53
+ }[];
54
+ };
55
+ /** Separado do IO para poder ser testado sem rede: dado um Brain, o que se mede. */
56
+ export declare function medeAsAncoras(raiz: string, brain: Brain): Promise<Medicao[]>;
57
+ /**
58
+ * Somente leitura, de proposito: e o que roda na ABERTURA da sessao. Medir e reportar
59
+ * ali dentro faria toda sessao escrever no servico antes de a pessoa digitar qualquer
60
+ * coisa — caro, e o tipo de efeito colateral que ninguem espera de "abrir o editor".
61
+ */
62
+ export declare function status(raiz: string): Promise<ResultadoDoStatus>;
63
+ export declare function check(raiz: string, commit?: string): Promise<ResultadoDoCheck>;
package/dist/check.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { pede } from "./api.js";
1
2
  import { leConfigDoRepo, leToken } from "./config.js";
2
3
  import { caminhosDoCommit, memoriasTocadas } from "./diff.js";
3
4
  import { mede } from "./medir.js";
@@ -29,7 +30,7 @@ async function buscaOBrain(raiz) {
29
30
  throw new Error(`sem credencial para ${config.api}. Rode \`dd-harness login --token <token>\`.`);
30
31
  }
31
32
  const parametros = `tenant=${encodeURIComponent(config.tenant)}&projeto=${encodeURIComponent(config.projeto)}`;
32
- const resposta = await fetch(`${config.api}/api/v1/artefatos?${parametros}`, {
33
+ const resposta = await pede(`${config.api}/api/v1/artefatos?${parametros}`, {
33
34
  headers: { Authorization: `Bearer ${token}` },
34
35
  });
35
36
  if (resposta.status === 401) {
@@ -49,7 +50,17 @@ export async function status(raiz) {
49
50
  const { brain } = await buscaOBrain(raiz);
50
51
  const agora = Date.now();
51
52
  const ativas = brain.memorias.filter((m) => m.status === "ativa");
53
+ const porPasta = new Map();
54
+ for (const m of ativas)
55
+ porPasta.set(m.pasta, (porPasta.get(m.pasta) ?? 0) + 1);
52
56
  return {
57
+ acervo: {
58
+ ativas: ativas.length,
59
+ arquivadas: brain.memorias.length - ativas.length,
60
+ porPasta: [...porPasta]
61
+ .map(([pasta, quantas]) => ({ pasta, quantas }))
62
+ .sort((a, b) => a.pasta.localeCompare(b.pasta)),
63
+ },
53
64
  comDeriva: ativas
54
65
  .filter((m) => (m.deriva_aberta ?? 0) > 0)
55
66
  .map((m) => ({
@@ -74,7 +85,7 @@ export async function check(raiz, commit) {
74
85
  if (medicoes.length === 0) {
75
86
  return { medidas: 0, ausentes: [], novas: 0, base: 0, jaAbertas: 0, fechadas: 0, tocadas };
76
87
  }
77
- const envio = await fetch(`${config.api}/api/v1/deriva`, {
88
+ const envio = await pede(`${config.api}/api/v1/deriva`, {
78
89
  method: "POST",
79
90
  headers: { ...cabecalhos, "Content-Type": "application/json" },
80
91
  body: JSON.stringify({
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Duas configuracoes, em lugares diferentes de proposito.
3
+ *
4
+ * `.dd-harness.json` fica **no repositorio** e e versionado: diz qual projeto aquele
5
+ * repo consome, e isso e informacao do repo, nao da pessoa.
6
+ *
7
+ * A credencial fica **na maquina** (`~/.dd-harness/credentials.json`, modo 0600) e nunca
8
+ * no repo: token e da pessoa e da maquina, e commitado seria segredo publicado.
9
+ */
10
+ export type ConfigDoRepo = {
11
+ api: string;
12
+ tenant: string;
13
+ projeto: string;
14
+ /** Onde o gerado vive. Muda-la move tudo de uma vez, menos o CLAUDE.md da raiz. */
15
+ pasta: string;
16
+ };
17
+ export declare const CAMINHO_CONFIG = ".dd-harness.json";
18
+ export declare const CAMINHO_MANIFESTO = "dd-harness/manifest.json";
19
+ export declare function leConfigDoRepo(raiz: string): Promise<ConfigDoRepo>;
20
+ /**
21
+ * Credencial por origem da API: a mesma maquina pode falar com uma instalacao local e
22
+ * com a de producao, e misturar token entre as duas daria 401 confuso.
23
+ */
24
+ export declare function leToken(api: string): Promise<string | null>;
25
+ export declare function guardaToken(api: string, token: string): Promise<string>;
26
+ export type Manifesto = {
27
+ etag: string | null;
28
+ arquivos: Record<string, string>;
29
+ };
30
+ export declare const manifestoVazio: () => Manifesto;
31
+ export declare function leManifesto(raiz: string): Promise<Manifesto>;
32
+ export declare function gravaManifesto(raiz: string, manifesto: Manifesto): Promise<void>;
33
+ export declare const hashDe: (conteudo: string) => string;
@@ -0,0 +1,12 @@
1
+ export declare function edita(raiz: string, caminho: string): Promise<{
2
+ endereco: string;
3
+ ancoras: number;
4
+ }>;
5
+ export type Arquivamento = {
6
+ motivo: "obsoleta" | "incorreta" | "fora_dos_filtros";
7
+ substituidaPor?: string;
8
+ };
9
+ export declare function arquiva(raiz: string, endereco: string, opcoes: Arquivamento): Promise<{
10
+ endereco: string;
11
+ motivo: string;
12
+ }>;
package/dist/curar.js ADDED
@@ -0,0 +1,77 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { relative } from "node:path";
3
+ import { cabecalhos, credencial, pede, recusa } from "./api.js";
4
+ import { gravaManifesto, hashDe, leManifesto } from "./config.js";
5
+ import { interpreta } from "./gravar.js";
6
+ /**
7
+ * Curadoria pelo agente: editar e arquivar.
8
+ *
9
+ * `gravar` sabia criar e mais nada. Uma memoria errada ficava errada, porque corrigir
10
+ * exigia abrir a interface — e o `CLAUDE.md` trata curadoria como obrigacao ("se
11
+ * encontrar uma memoria obsoleta ou errada, corrija").
12
+ *
13
+ * Editar reaproveita o mesmo markdown de `gravar`, de proposito: o agente edita o arquivo
14
+ * que o `sync` materializou e manda de volta. Um formato so para as duas operacoes, e o
15
+ * que ele ja sabe ler.
16
+ */
17
+ /**
18
+ * Depois que o servico aceita, o arquivo em disco deixa de ser "edicao nao enviada" — e o
19
+ * manifesto tem que saber, senao o `sync` seguinte para com "editado a mao" e a unica saida
20
+ * que resta e descartar o arquivo. O ciclo materializa-corrige-envia travava justamente
21
+ * aqui, com o servico ja atualizado.
22
+ *
23
+ * Guarda o hash do que foi enviado, nao do que o servico devolveria: o `sync` seguinte
24
+ * reescreve o arquivo na forma canonica quando o ETag mudar.
25
+ */
26
+ async function marcaComoEnviado(raiz, caminho, conteudo) {
27
+ const chave = relative(raiz, caminho).split("\\").join("/");
28
+ const manifesto = await leManifesto(raiz);
29
+ // Arquivo de fora do repositorio (um rascunho em /tmp, por exemplo) nao esta no
30
+ // manifesto e nao deve entrar: o manifesto descreve o que o `sync` materializou.
31
+ if (!manifesto.arquivos[chave])
32
+ return;
33
+ manifesto.arquivos[chave] = hashDe(conteudo);
34
+ await gravaManifesto(raiz, manifesto);
35
+ }
36
+ export async function edita(raiz, caminho) {
37
+ const { config, token } = await credencial(raiz);
38
+ const cru = await readFile(caminho, "utf8");
39
+ const memoria = interpreta(cru);
40
+ const endereco = `${memoria.pasta}/${memoria.slug}`;
41
+ const resposta = await pede(`${config.api}/api/v1/memorias/${endereco}`, {
42
+ method: "PATCH",
43
+ headers: cabecalhos(token, true),
44
+ body: JSON.stringify({
45
+ tenant: config.tenant,
46
+ projeto: config.projeto,
47
+ titulo: memoria.titulo,
48
+ resumo: memoria.resumo,
49
+ corpo: memoria.corpo,
50
+ dano: memoria.dano,
51
+ invisibilidade: memoria.invisibilidade,
52
+ externalidade: memoria.externalidade,
53
+ ancoras: memoria.ancoras,
54
+ }),
55
+ });
56
+ if (!resposta.ok)
57
+ await recusa(resposta);
58
+ await marcaComoEnviado(raiz, caminho, cru);
59
+ return { endereco, ancoras: memoria.ancoras.length };
60
+ }
61
+ export async function arquiva(raiz, endereco, opcoes) {
62
+ const { config, token } = await credencial(raiz);
63
+ const resposta = await pede(`${config.api}/api/v1/memorias/${endereco}`, {
64
+ method: "DELETE",
65
+ headers: cabecalhos(token, true),
66
+ body: JSON.stringify({
67
+ tenant: config.tenant,
68
+ projeto: config.projeto,
69
+ motivo: opcoes.motivo,
70
+ ...(opcoes.substituidaPor ? { substituida_por: opcoes.substituidaPor } : {}),
71
+ }),
72
+ });
73
+ if (!resposta.ok)
74
+ await recusa(resposta);
75
+ const lido = (await resposta.json());
76
+ return lido;
77
+ }
package/dist/diff.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ import type { Brain } from "./materializa.js";
2
+ /**
3
+ * Quais memorias falam do que este commit mudou.
4
+ *
5
+ * E a peca que devolve a memoria ao code review. Tirar o Brain do repositorio tirou o
6
+ * revisor que o diff dava de graca — no modelo file-based a memoria estava ali, no
7
+ * caminho de quem mexia. Aqui ela volta pelo cruzamento: caminho do diff contra ancora.
8
+ *
9
+ * Repare que isto NAO e deriva. Deriva e "o alvo mudou desde a linha de base"; isto e
10
+ * "voce acabou de mexer no que esta memoria guarda" — util mesmo quando a base ainda
11
+ * nem foi medida, e util mesmo que a memoria continue valendo.
12
+ */
13
+ /** Caminhos alterados num commit. Vazio quando o git nao responde — nunca quebra. */
14
+ export declare function caminhosDoCommit(raiz: string, commit: string): Promise<string[]>;
15
+ export type Tocada = {
16
+ pasta: string;
17
+ memoria: string;
18
+ titulo: string;
19
+ ancora: string;
20
+ };
21
+ /**
22
+ * Cruza caminhos com ancoras. A ancora casa quando e o proprio caminho ou quando e um
23
+ * diretorio que o contem — `supabase/migrations` tem que casar com a migration nova.
24
+ */
25
+ export declare function memoriasTocadas(brain: Brain, caminhos: string[]): Tocada[];
@@ -0,0 +1,30 @@
1
+ /**
2
+ * `dd-harness gravar <arquivo.md>` — o caminho pelo qual o agente registra memoria.
3
+ *
4
+ * A entrada e um markdown no mesmo formato que o `sync` materializa, porque e o formato
5
+ * que o agente ja sabe ler: frontmatter, corpo, os tres filtros, ancoras. Escrever um
6
+ * arquivo e o que ele fazia no modelo file-based; aqui o arquivo vira requisicao em vez
7
+ * de virar commit.
8
+ *
9
+ * O arquivo nao e deixado no repositorio: quem materializa e o `sync`, a partir do
10
+ * servico. Gravar e depois sincronizar traz a memoria de volta ja com o endereco real —
11
+ * e evita a divergencia de ter uma copia escrita a mao ao lado da copia gerada.
12
+ */
13
+ export type MemoriaLida = {
14
+ slug: string;
15
+ titulo: string;
16
+ resumo: string;
17
+ pasta: string;
18
+ corpo: string;
19
+ dano: string;
20
+ invisibilidade: string;
21
+ externalidade: string;
22
+ ancoras: string[];
23
+ tambemEm: string[];
24
+ };
25
+ export declare function interpreta(texto: string): MemoriaLida;
26
+ export declare function grava(raiz: string, caminho: string): Promise<{
27
+ endereco: string;
28
+ ancoras: number;
29
+ projetos: number;
30
+ }>;
package/dist/gravar.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { readFile } from "node:fs/promises";
2
- import { leConfigDoRepo, leToken } from "./config.js";
2
+ import { cabecalhos, credencial, pede, recusa } from "./api.js";
3
3
  const OBRIGATORIOS = ["name", "titulo", "description", "pasta"];
4
4
  /** Frontmatter simples: `chave: valor` por linha. Sem lib de YAML — nao ha aninhamento. */
5
5
  function leFrontmatter(texto) {
@@ -74,15 +74,11 @@ export function interpreta(texto) {
74
74
  };
75
75
  }
76
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
- }
77
+ const { config, token } = await credencial(raiz);
82
78
  const memoria = interpreta(await readFile(caminho, "utf8"));
83
- const resposta = await fetch(`${config.api}/api/v1/memorias`, {
79
+ const resposta = await pede(`${config.api}/api/v1/memorias`, {
84
80
  method: "POST",
85
- headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
81
+ headers: cabecalhos(token, true),
86
82
  body: JSON.stringify({
87
83
  tenant: config.tenant,
88
84
  projeto: config.projeto,
@@ -98,10 +94,8 @@ export async function grava(raiz, caminho) {
98
94
  tambem_em: memoria.tambemEm,
99
95
  }),
100
96
  });
101
- if (!resposta.ok) {
102
- const { erro } = (await resposta.json().catch(() => ({})));
103
- throw new Error(erro ?? `a API respondeu ${resposta.status}.`);
104
- }
97
+ if (!resposta.ok)
98
+ await recusa(resposta);
105
99
  const { endereco } = (await resposta.json());
106
100
  return {
107
101
  endereco,
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js CHANGED
@@ -2,8 +2,12 @@
2
2
  import { check, status } from "./check.js";
3
3
  import { leConfigDoRepo, guardaToken } from "./config.js";
4
4
  import { grava } from "./gravar.js";
5
- import { init } from "./init.js";
5
+ import { init, SUGESTAO_MCP } from "./init.js";
6
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";
10
+ import { criaProjeto } from "./projeto.js";
7
11
  import { sync } from "./sync.js";
8
12
  /**
9
13
  * `dd-harness` — o cliente que materializa os artefatos no repositorio.
@@ -15,13 +19,23 @@ import { sync } from "./sync.js";
15
19
  */
16
20
  const AJUDA = `dd-harness — materializa política, briefing e Brain no repositório
17
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)
18
26
  dd-harness init --tenant <t> --projeto <p> [--api <url>]
19
27
  prepara o repositório (config + CLAUDE.md)
20
- dd-harness login --token <token> guarda a credencial desta máquina
28
+ dd-harness pasta <slug> --definicao "o que entra e o que não entra"
29
+ cria a pasta que o gravar exige
21
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
22
36
  dd-harness sync escreve os artefatos em disco
23
37
  dd-harness check [--commit <sha>] mede as âncoras e reporta a deriva
24
- dd-harness status só lê: o que espera julgamento
38
+ dd-harness status só lê: o tamanho do Brain e o que espera julgamento
25
39
  dd-harness --help
26
40
 
27
41
  O gerado vive em dd-harness/. Na raiz fica só o seu CLAUDE.md, que importa a
@@ -31,6 +45,19 @@ política com a linha ${LINHA_DE_IMPORT}
31
45
  function avisaSobreOPonteiro(ponteiro) {
32
46
  if (ponteiro === "ok" || ponteiro === "sem-politica")
33
47
  return;
48
+ // O import pendurado e o inverso dos outros dois: a linha esta la, o alvo e que nao
49
+ // existe. Dizer "acrescente a linha" aqui mandaria a pessoa para o lugar errado.
50
+ if (ponteiro === "aponta-para-o-vazio") {
51
+ console.error([
52
+ "",
53
+ `AVISO: o CLAUDE.md importa ${LINHA_DE_IMPORT}, mas não há política no serviço.`,
54
+ "O arquivo apontado não existe, e import quebrado falha em silêncio: a sessão abre",
55
+ "sem protocolo e nada avisa.",
56
+ "",
57
+ "Escreva a política do projeto no serviço, ou tire a linha do CLAUDE.md.",
58
+ ].join("\n"));
59
+ return;
60
+ }
34
61
  const motivo = ponteiro === "sem-claude-md"
35
62
  ? "não há CLAUDE.md na raiz"
36
63
  : "o CLAUDE.md da raiz não importa a política";
@@ -83,14 +110,81 @@ async function comandoInit(argv) {
83
110
  "ja-tinha-a-linha": "mantido CLAUDE.md — já importava a política",
84
111
  }[r.claudeMd]);
85
112
  console.log("\nAgora: dd-harness login --token <token> && dd-harness sync");
113
+ if (r.mcp === "ja-declarado") {
114
+ console.log("\nmantido .mcp.json — o servidor dd-harness já está declarado");
115
+ return;
116
+ }
117
+ console.log("\nOpcional: as memórias como ferramenta, para o agente buscar e gravar sem" +
118
+ "\nescrever arquivo. Acrescente ao `.mcp.json` da raiz (não escrevo nele: o" +
119
+ "\narquivo é seu e pode declarar outros servidores):\n");
120
+ console.log(SUGESTAO_MCP);
86
121
  }
122
+ /**
123
+ * Nao exige `.dd-harness.json`, e isso importa: sem projeto no servico o `init` nao tem o
124
+ * que apontar, criar projeto exige credencial, e credencial exigindo config fechava um
125
+ * ciclo sem entrada — no dia um de um repositorio novo, nenhum dos tres rodava. O `--api`
126
+ * resolve; com config no repo, ela preenche.
127
+ */
87
128
  async function comandoLogin(argv) {
88
129
  const token = argumento(argv, "token");
89
130
  if (!token)
90
- throw new Error("uso: dd-harness login --token <token>");
91
- const config = await leConfigDoRepo(process.cwd());
92
- const caminho = await guardaToken(config.api, token);
93
- console.log(`credencial de ${config.api} guardada em ${caminho}`);
131
+ throw new Error("uso: dd-harness login --token <token> [--api <url>]");
132
+ const daLinha = argumento(argv, "api");
133
+ const doRepo = daLinha ? null : await leConfigDoRepo(process.cwd()).catch(() => null);
134
+ const api = (daLinha ?? doRepo?.api ?? "https://dd-harness.vercel.app").replace(/\/$/, "");
135
+ const caminho = await guardaToken(api, token);
136
+ console.log(`credencial de ${api} guardada em ${caminho}`);
137
+ }
138
+ async function comandoEditar(argv) {
139
+ const caminho = argv[0];
140
+ if (!caminho || caminho.startsWith("-")) {
141
+ throw new Error("uso: dd-harness editar <arquivo.md>");
142
+ }
143
+ const r = await edita(process.cwd(), caminho);
144
+ console.log(`editado ${r.endereco}`);
145
+ console.log(` ${r.ancoras} âncora(s). Rode \`dd-harness sync\` para materializar.`);
146
+ }
147
+ const MOTIVOS = ["obsoleta", "incorreta", "fora_dos_filtros"];
148
+ async function comandoArquivar(argv) {
149
+ const endereco = argv[0];
150
+ const motivo = argumento(argv, "motivo");
151
+ if (!endereco || endereco.startsWith("-") || !endereco.includes("/")) {
152
+ throw new Error("uso: dd-harness arquivar <pasta>/<slug> --motivo <obsoleta|incorreta|fora_dos_filtros>");
153
+ }
154
+ // Lista fechada tambem no cliente: errar o motivo aqui devolve a lista, em vez de um
155
+ // 400 do servidor com a mesma informacao mais longe de quem digitou.
156
+ if (!motivo || !MOTIVOS.includes(motivo)) {
157
+ throw new Error(`--motivo precisa ser um de: ${MOTIVOS.join(", ")}`);
158
+ }
159
+ const r = await arquiva(process.cwd(), endereco, {
160
+ motivo: motivo,
161
+ substituidaPor: argumento(argv, "substituida-por"),
162
+ });
163
+ console.log(`arquivado ${r.endereco} (${r.motivo})`);
164
+ console.log(" foi para o histórico, não foi apagada. Rode `dd-harness sync`.");
165
+ }
166
+ async function comandoBuscar(argv) {
167
+ const consulta = argv.filter((a) => !a.startsWith("--")).join(" ").trim();
168
+ if (!consulta)
169
+ throw new Error('uso: dd-harness buscar "<pergunta>"');
170
+ const limite = Number(argumento(argv, "limite")) || undefined;
171
+ const r = await busca(process.cwd(), consulta, limite);
172
+ if (!r.achados.length) {
173
+ console.log("nada encontrado.");
174
+ // Sem esta linha, "nada encontrado" vira "nao existe" — quando pode ser so a
175
+ // semantica desligada, e a memoria estar la com outras palavras.
176
+ if (!r.semantica) {
177
+ console.log(" (só busca textual: sem provedor de embedding no serviço)");
178
+ }
179
+ return;
180
+ }
181
+ console.log(r.semantica
182
+ ? `${r.achados.length} resultado(s) — busca híbrida (${r.provedor}):`
183
+ : `${r.achados.length} resultado(s) — só textual, sem semântica:`);
184
+ for (const a of r.achados) {
185
+ console.log(` ${a.endereco} — ${a.titulo}`);
186
+ console.log(` ${a.resumo}`);
187
+ }
94
188
  }
95
189
  async function comandoSync() {
96
190
  const resultado = await sync(process.cwd());
@@ -99,8 +193,11 @@ async function comandoSync() {
99
193
  "parei sem escrever nada: estes arquivos foram editados à mão.",
100
194
  ...resultado.arquivos.map((a) => ` ${a}`),
101
195
  "",
102
- "O disco é projeção do serviço, uma direção só. Leve a mudança para o serviço,",
103
- "ou descarte a edição local (git checkout / apague o arquivo) e sincronize de novo.",
196
+ "O disco é projeção do serviço, uma direção só. Duas saídas:",
197
+ "",
198
+ " 1. Leve a edição para o serviço — `dd-harness editar <arquivo>` para cada um",
199
+ " acima. É o caminho normal de corrigir memória, e destrava o sync.",
200
+ " 2. Descarte a edição local (git checkout / apague o arquivo) e sincronize.",
104
201
  ].join("\n"));
105
202
  process.exitCode = 1;
106
203
  return;
@@ -166,10 +263,24 @@ async function comandoCheck(argv) {
166
263
  }
167
264
  async function comandoStatus() {
168
265
  const r = await status(process.cwd());
266
+ // O acervo vem primeiro, e sempre: e a resposta para "o que ha no Brain deste projeto?",
267
+ // que era a pergunta sem comando. Sem ela, quem quer saber conta arquivo em disco — e
268
+ // acerta por acidente, porque memoria arquivada sai do disco e continua no indice.
269
+ const { ativas, arquivadas, porPasta } = r.acervo;
270
+ if (ativas === 0 && arquivadas === 0) {
271
+ console.log("dd-harness: o Brain deste projeto está vazio.");
272
+ }
273
+ else {
274
+ const detalhe = porPasta.map((p) => `${p.pasta} ${p.quantas}`).join(", ");
275
+ console.log(`dd-harness: ${ativas} memória(s) ativa(s)` +
276
+ (arquivadas ? ` e ${arquivadas} arquivada(s)` : "") +
277
+ (detalhe ? ` — ${detalhe}` : ""));
278
+ }
169
279
  if (!r.comDeriva.length && !r.vencidas.length) {
170
- console.log("dd-harness: nada esperando julgamento.");
280
+ console.log("Nada esperando julgamento.");
171
281
  return;
172
282
  }
283
+ console.log("");
173
284
  if (r.comDeriva.length) {
174
285
  const total = r.comDeriva.reduce((soma, m) => soma + m.abertas, 0);
175
286
  console.log(`dd-harness: ${total} deriva(s) aberta(s), esperando julgamento:`);
@@ -184,6 +295,36 @@ async function comandoStatus() {
184
295
  console.log(` ${m.pasta}/${m.memoria} — ${m.titulo}`);
185
296
  }
186
297
  }
298
+ /**
299
+ * Vem ANTES do `init`, e nao depois: o `init` escreve o `.dd-harness.json` apontando para
300
+ * um projeto, e apontar para projeto que nao existe deixaria todo comando seguinte em 404.
301
+ */
302
+ async function comandoProjeto(argv) {
303
+ const slug = argv[0];
304
+ const nome = argumento(argv, "nome");
305
+ if (!slug || slug.startsWith("-") || !nome) {
306
+ throw new Error('uso: dd-harness projeto <slug> --nome "<nome>" [--tenant <slug>] [--api <url>]');
307
+ }
308
+ const r = await criaProjeto(process.cwd(), slug, nome, {
309
+ tenant: argumento(argv, "tenant"),
310
+ api: argumento(argv, "api"),
311
+ });
312
+ console.log(r.jaExistia
313
+ ? `projeto ${r.projeto} já existia — nada criado.`
314
+ : `criado projeto ${r.projeto}.`);
315
+ console.log(`Agora: dd-harness init --tenant <espaço> --projeto ${r.projeto}`);
316
+ }
317
+ async function comandoPasta(argv) {
318
+ const slug = argv[0];
319
+ const definicao = argumento(argv, "definicao");
320
+ if (!slug || slug.startsWith("-") || !definicao) {
321
+ throw new Error('uso: dd-harness pasta <slug> --definicao "o que entra e o que não entra"');
322
+ }
323
+ const r = await criaPasta(process.cwd(), slug, definicao);
324
+ console.log(r.jaExistia
325
+ ? `pasta ${r.pasta} já existia — nada criado.`
326
+ : `criada pasta ${r.pasta}. Agora \`dd-harness gravar <arquivo.md>\` aceita \`pasta: ${r.pasta}\`.`);
327
+ }
187
328
  /**
188
329
  * O agente escreve o arquivo — que e o que ele ja fazia no modelo file-based — e este
189
330
  * comando o transforma em requisicao. O arquivo nao fica no repositorio: quem
@@ -207,8 +348,18 @@ async function principal() {
207
348
  return comandoInit(resto);
208
349
  case "login":
209
350
  return comandoLogin(resto);
351
+ case "projeto":
352
+ return comandoProjeto(resto);
353
+ case "pasta":
354
+ return comandoPasta(resto);
210
355
  case "gravar":
211
356
  return comandoGravar(resto);
357
+ case "editar":
358
+ return comandoEditar(resto);
359
+ case "arquivar":
360
+ return comandoArquivar(resto);
361
+ case "buscar":
362
+ return comandoBuscar(resto);
212
363
  case "sync":
213
364
  return comandoSync();
214
365
  case "check":
package/dist/init.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ export type ResultadoDoInit = {
2
+ config: "criada" | "ja-existia";
3
+ claudeMd: "criado" | "linha-acrescentada" | "ja-tinha-a-linha";
4
+ mcp: "ja-declarado" | "a-declarar";
5
+ };
6
+ /**
7
+ * O `.mcp.json` e sugerido, nunca escrito.
8
+ *
9
+ * Pelo mesmo motivo do `CLAUDE.md` da raiz: o arquivo e do repositorio, pode ja declarar
10
+ * outros servidores, e mesclar JSON alheio e onde falha silenciosa nasce — entrada errada
11
+ * nao da erro, a ferramenta so nao aparece. Quem cola sabe o que colou.
12
+ */
13
+ export declare const SUGESTAO_MCP = "{\n \"mcpServers\": {\n \"dd-harness\": {\n \"command\": \"npx\",\n \"args\": [\"-y\", \"dd-harness-mcp\"]\n }\n }\n}";
14
+ export declare function init(raiz: string, dados: {
15
+ tenant: string;
16
+ projeto: string;
17
+ api?: string;
18
+ }): Promise<ResultadoDoInit>;
package/dist/init.js CHANGED
@@ -17,6 +17,32 @@ Este arquivo é seu: escreva aqui o que for específico deste repositório.
17
17
  A linha abaixo importa a política gerenciada pelo dd-harness. **Não a remova** — sem
18
18
  ela a sessão abre sem protocolo, e nada avisa.
19
19
  `;
20
+ /**
21
+ * O `.mcp.json` e sugerido, nunca escrito.
22
+ *
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.
26
+ */
27
+ export const SUGESTAO_MCP = `{
28
+ "mcpServers": {
29
+ "dd-harness": {
30
+ "command": "npx",
31
+ "args": ["-y", "dd-harness-mcp"]
32
+ }
33
+ }
34
+ }`;
35
+ async function declaraMcp(raiz) {
36
+ try {
37
+ const cru = await readFile(join(raiz, ".mcp.json"), "utf8");
38
+ const lido = JSON.parse(cru);
39
+ return lido.mcpServers?.["dd-harness"] ? "ja-declarado" : "a-declarar";
40
+ }
41
+ catch {
42
+ // Sem arquivo, ou JSON que nao interpreta: nos dois casos ha o que sugerir.
43
+ return "a-declarar";
44
+ }
45
+ }
20
46
  export async function init(raiz, dados) {
21
47
  const caminhoConfig = join(raiz, CAMINHO_CONFIG);
22
48
  let config = "ja-existia";
@@ -54,5 +80,5 @@ export async function init(raiz, dados) {
54
80
  await writeFile(caminhoClaude, `${atual}${separador}${LINHA_DE_IMPORT}\n`, "utf8");
55
81
  claudeMd = "linha-acrescentada";
56
82
  }
57
- return { config, claudeMd };
83
+ return { config, claudeMd, mcp: await declaraMcp(raiz) };
58
84
  }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Do payload do contrato para arquivos em disco.
3
+ *
4
+ * Funcao pura de proposito: recebe o Brain e devolve caminho -> conteudo, sem rede e sem
5
+ * `fs`. E a parte que precisa de teste — o formato tem que casar com o que o
6
+ * `validate_brain.cjs` do molde espera (frontmatter `name` igual ao arquivo, `pasta`
7
+ * igual a pasta que o contem, e uma linha de indice por memoria).
8
+ */
9
+ export type Ancora = {
10
+ tipo: string;
11
+ valor: string;
12
+ sha: string | null;
13
+ };
14
+ export type Memoria = {
15
+ pasta: string;
16
+ slug: string;
17
+ titulo: string;
18
+ resumo: string;
19
+ corpo: string;
20
+ status: "ativa" | "historico";
21
+ dano: string;
22
+ invisibilidade: string;
23
+ externalidade: string;
24
+ ancoras: Ancora[];
25
+ revisar_ate: string | null;
26
+ /** Observacoes de deriva esperando julgamento. Ausente em payload antigo. */
27
+ deriva_aberta?: number;
28
+ };
29
+ export type Brain = {
30
+ tenant: {
31
+ slug: string;
32
+ nome: string;
33
+ };
34
+ projeto: {
35
+ slug: string;
36
+ nome: string;
37
+ };
38
+ /** `CLAUDE.md`. Nulo ou vazio: ainda nao existe, e nao vira arquivo. */
39
+ politica?: string | null;
40
+ /** `BRIEFING.md`. Mesma regra. */
41
+ briefing?: string | null;
42
+ pastas: {
43
+ slug: string;
44
+ definicao: string;
45
+ }[];
46
+ memorias: Memoria[];
47
+ };
48
+ export declare function arquivoDaMemoria(m: Memoria): string;
49
+ export declare function arquivoDoIndice(brain: Brain): string;
50
+ /** Tudo o que o servico gera vive aqui — e nada fora daqui e escrito pelo `sync`. */
51
+ export declare const PASTA = "dd-harness";
52
+ /** O que a raiz precisa conter para a politica chegar a sessao. */
53
+ export declare const LINHA_DE_IMPORT = "@dd-harness/politica.md";
54
+ /**
55
+ * Caminho relativo (POSIX) -> conteúdo. As chaves são o que o manifesto guarda.
56
+ *
57
+ * Tudo dentro de `dd-harness/`, inclusive a política. Na raiz fica só o `CLAUDE.md`, que
58
+ * é **seu**: o `sync` não o escreve, apenas confere que ele importa a política. É o que
59
+ * deixa conviverem a parte gerenciada e o que aquele repositório tem de próprio — e o
60
+ * que faz adotar um projeto existente ser uma linha, não um ritual.
61
+ *
62
+ * Artefato vazio ou ausente não entra no mapa; como o manifesto remove o que saiu do
63
+ * conjunto, esvaziar no serviço apaga o arquivo no próximo `sync`.
64
+ */
65
+ export declare function materializa(brain: Brain, pasta?: string): Map<string, string>;
@@ -28,9 +28,14 @@ function secaoDeFiltros(m) {
28
28
  ].join("\n");
29
29
  }
30
30
  export function arquivoDaMemoria(m) {
31
+ // `titulo` vai no frontmatter porque `gravar` e `editar` o exigem la: sem ele o arquivo
32
+ // materializado nao volta pelo `editar`, e o ciclo "sincroniza, corrige, manda de volta"
33
+ // — que e como o agente cura memoria — para com "frontmatter sem `titulo`". O titulo
34
+ // tambem aparece no indice, mas indice nao e o que se edita.
31
35
  const frontmatter = [
32
36
  "---",
33
37
  `name: ${m.slug}`,
38
+ `titulo: ${m.titulo.replace(/\n/g, " ")}`,
34
39
  `description: ${m.resumo.replace(/\n/g, " ")}`,
35
40
  `pasta: ${m.pasta}`,
36
41
  ...(m.revisar_ate ? [`revisar-ate: ${m.revisar_ate.slice(0, 10)}`] : []),
@@ -54,12 +59,19 @@ export function arquivoDoIndice(brain) {
54
59
  const ativas = brain.memorias.filter((m) => m.status === "ativa");
55
60
  const historico = brain.memorias.filter((m) => m.status === "historico");
56
61
  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);
62
+ // O indice e montado pelas pastas, mas quem manda e a memoria: pasta que so aparece no
63
+ // `pasta:` de uma memoria tambem vira secao. Sem isso a memoria fica em disco e fora do
64
+ // indice arquivo presente que nenhum agente encontra, que e a mesma falha silenciosa
65
+ // do import quebrado.
66
+ const definicoes = new Map(brain.pastas.map((p) => [p.slug, p.definicao]));
67
+ const slugs = [...new Set([...definicoes.keys(), ...ativas.map((m) => m.pasta)])].sort();
68
+ const secoes = slugs
69
+ .map((slug) => {
70
+ const memorias = ativas.filter((m) => m.pasta === slug);
71
+ const definicao = definicoes.get(slug);
60
72
  return [
61
- `## ${pasta.slug}`,
62
- `<!-- ${pasta.definicao} -->`,
73
+ `## ${slug}`,
74
+ `<!-- ${definicao ?? "(definição não veio do serviço)"} -->`,
63
75
  ...(memorias.length ? memorias.map(linha) : ["<!-- (vazia) -->"]),
64
76
  ].join("\n");
65
77
  })
@@ -0,0 +1 @@
1
+ export declare function mede(raiz: string, valor: string): Promise<string | null>;
package/dist/medir.js CHANGED
@@ -32,7 +32,48 @@ async function hashDeDiretorio(caminho) {
32
32
  await anda(caminho, "");
33
33
  return createHash("sha256").update(nomes.sort().join("\n")).digest("hex");
34
34
  }
35
+ /**
36
+ * Trecho: `caminho#alvo` — hash das LINHAS que contem `alvo`, nao do arquivo.
37
+ *
38
+ * Existe porque ancora de arquivo mede grosso demais. Medido: renomear a variavel de um
39
+ * laco disparou as 5 memorias ancoradas naquele arquivo, nenhuma delas desatualizada. Cada
40
+ * uma falava de um trecho diferente, e o hash do arquivo nao distingue.
41
+ *
42
+ * `null` quando o alvo nao aparece mais: e o mesmo sinal de "arquivo ausente" — o trecho que
43
+ * a memoria descreve deixou de existir, e o servico decide o que isso significa. Trecho que
44
+ * some por rename e deriva legitima, nao falso positivo: a memoria aponta para algo que
45
+ * nao esta mais la.
46
+ *
47
+ * Comparacao literal, sem regex: o alvo vem de quem escreveu a memoria, e regex daria
48
+ * poder de travar o `check` (catastrophic backtracking) a quem so queria apontar uma linha.
49
+ */
50
+ async function hashDeTrecho(caminho, alvo) {
51
+ const conteudo = await readFile(caminho, "utf8");
52
+ const casam = conteudo
53
+ .split(/\r?\n/)
54
+ .filter((linha) => linha.includes(alvo))
55
+ .map((linha) => linha.trim());
56
+ if (casam.length === 0)
57
+ return null;
58
+ return createHash("sha256").update(casam.join("\n"), "utf8").digest("hex");
59
+ }
35
60
  export async function mede(raiz, valor) {
61
+ // O `#` separa alvo de caminho. O CHECK do banco garante um so, e nenhum lado vazio.
62
+ const corte = valor.indexOf("#");
63
+ if (corte !== -1) {
64
+ const caminho = join(raiz, valor.slice(0, corte));
65
+ const alvo = valor.slice(corte + 1);
66
+ try {
67
+ const info = await stat(caminho);
68
+ // Trecho de diretorio nao existe: o alvo e texto dentro de um arquivo.
69
+ if (info.isDirectory())
70
+ return null;
71
+ return await hashDeTrecho(caminho, alvo);
72
+ }
73
+ catch {
74
+ return null;
75
+ }
76
+ }
36
77
  const caminho = join(raiz, valor);
37
78
  try {
38
79
  const info = await stat(caminho);
@@ -0,0 +1,11 @@
1
+ /**
2
+ * `dd-harness pasta <slug> --definicao "..."` — cria a pasta sem sair da sessao.
3
+ *
4
+ * `gravar` recusa quando a pasta nao existe, e essa recusa e boa: e ela que impede um
5
+ * typo no `pasta:` de virar pasta nova em silencio. O que faltava era a saida — ate aqui
6
+ * o agente parava e pedia que alguem abrisse o navegador.
7
+ */
8
+ export declare function criaPasta(raiz: string, slug: string, definicao: string): Promise<{
9
+ pasta: string;
10
+ jaExistia: boolean;
11
+ }>;
package/dist/pasta.js ADDED
@@ -0,0 +1,30 @@
1
+ import { cabecalhos, credencial, pede, recusa } from "./api.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, token } = await credencial(raiz);
11
+ const resposta = await pede(`${config.api}/api/v1/pastas`, {
12
+ method: "POST",
13
+ headers: cabecalhos(token, true),
14
+ body: JSON.stringify({
15
+ tenant: config.tenant,
16
+ projeto: config.projeto,
17
+ slug,
18
+ definicao,
19
+ }),
20
+ });
21
+ // Pasta que ja existe nao e falha para quem chamou: o estado desejado ja vale. Vale
22
+ // dizer que ja existia — quem pediu pode estar corrigindo um typo e precisa saber que
23
+ // nao criou nada.
24
+ if (resposta.status === 409)
25
+ return { pasta: slug, jaExistia: true };
26
+ if (!resposta.ok)
27
+ await recusa(resposta);
28
+ const { pasta } = (await resposta.json());
29
+ return { pasta, jaExistia: false };
30
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `dd-harness projeto <slug> --nome "..."` — cria o projeto sem sair da sessao.
3
+ *
4
+ * Era o ultimo passo do espaco que exigia navegador. O tenant continua manual de
5
+ * proposito (fronteira de isolamento e ato de dono), mas daqui para baixo o agente que
6
+ * abre um repositorio novo consegue preparar tudo: projeto, pasta e memoria.
7
+ *
8
+ * Nao escreve o `.dd-harness.json`: quem faz isso e o `init`, e ele precisa de um projeto
9
+ * que ja exista. A ordem e `projeto` e depois `init` — e por isso este comando NAO pode
10
+ * exigir o config do repo, que nesse momento ainda nao existe. Daí `--tenant` e `--api`:
11
+ * sem config, eles vem da linha de comando; com config, ele preenche o que faltar.
12
+ */
13
+ export declare function criaProjeto(raiz: string, slug: string, nome: string, explicito?: {
14
+ tenant?: string;
15
+ api?: string;
16
+ }): Promise<{
17
+ projeto: string;
18
+ jaExistia: boolean;
19
+ }>;
@@ -0,0 +1,39 @@
1
+ import { cabecalhos, pede, recusa } from "./api.js";
2
+ import { leConfigDoRepo, leToken } from "./config.js";
3
+ /**
4
+ * `dd-harness projeto <slug> --nome "..."` — cria o projeto sem sair da sessao.
5
+ *
6
+ * Era o ultimo passo do espaco que exigia navegador. O tenant continua manual de
7
+ * proposito (fronteira de isolamento e ato de dono), mas daqui para baixo o agente que
8
+ * abre um repositorio novo consegue preparar tudo: projeto, pasta e memoria.
9
+ *
10
+ * Nao escreve o `.dd-harness.json`: quem faz isso e o `init`, e ele precisa de um projeto
11
+ * que ja exista. A ordem e `projeto` e depois `init` — e por isso este comando NAO pode
12
+ * exigir o config do repo, que nesse momento ainda nao existe. Daí `--tenant` e `--api`:
13
+ * sem config, eles vem da linha de comando; com config, ele preenche o que faltar.
14
+ */
15
+ export async function criaProjeto(raiz, slug, nome, explicito = {}) {
16
+ const doRepo = await leConfigDoRepo(raiz).catch(() => null);
17
+ const tenant = explicito.tenant ?? doRepo?.tenant;
18
+ const api = (explicito.api ?? doRepo?.api ?? "https://dd-harness.vercel.app").replace(/\/$/, "");
19
+ if (!tenant) {
20
+ throw new Error("não sei em que espaço criar: passe `--tenant <slug>` (ou rode dentro de um repositório já com .dd-harness.json).");
21
+ }
22
+ const token = await leToken(api);
23
+ if (!token) {
24
+ throw new Error(`sem credencial para ${api}. Rode \`dd-harness login --token <token>\`.`);
25
+ }
26
+ const resposta = await pede(`${api}/api/v1/projetos`, {
27
+ method: "POST",
28
+ headers: cabecalhos(token, true),
29
+ body: JSON.stringify({ tenant, slug, nome }),
30
+ });
31
+ // Como em `pasta`: projeto que ja existe nao e falha para quem chamou, o estado desejado
32
+ // ja vale. Vale dizer que existia — quem pediu pode estar corrigindo um typo.
33
+ if (resposta.status === 409)
34
+ return { projeto: slug, jaExistia: true };
35
+ if (!resposta.ok)
36
+ await recusa(resposta);
37
+ const { projeto } = (await resposta.json());
38
+ return { projeto, jaExistia: false };
39
+ }
package/dist/sync.d.ts ADDED
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Busca o Brain e escreve em disco. Uma direcao so.
3
+ *
4
+ * O manifesto guarda o hash de cada arquivo que este comando escreveu. Antes de
5
+ * sobrescrever, ele confere: hash igual ao guardado significa "isto e meu, posso
6
+ * reescrever"; hash diferente significa que alguem editou a mao, e ai o comando **para**.
7
+ * Deriva silenciosa entre repo e servico e exatamente o que este projeto existe para
8
+ * evitar — melhor falhar alto do que engolir a edicao de alguem.
9
+ */
10
+ /**
11
+ * Estado do ponteiro na raiz.
12
+ *
13
+ * `CLAUDE.md` nao e gerado — e do repositorio, e so precisa importar a politica. Mas o
14
+ * import falha em **silencio**: sem o arquivo alvo, ou sem a linha, a sessao abre e
15
+ * nada avisa que a politica nao veio junto. Foi medido, nao suposto. Como a sessao sem
16
+ * protocolo e a falha que o molde mais combate, o `sync` confere isso toda vez — mesmo
17
+ * quando o servico nao mudou, porque quem apaga a linha e quem mexe no repositorio.
18
+ */
19
+ export type Ponteiro = "ok" | "sem-claude-md" | "sem-a-linha" | "sem-politica"
20
+ /**
21
+ * A linha de import existe, mas nao ha politica no servico para ela apontar. O `init`
22
+ * escreve a linha antes de existir politica, entao o repositorio novo cai aqui: import
23
+ * pendurado desde o primeiro dia, e o mesmo silencio de sempre.
24
+ */
25
+ | "aponta-para-o-vazio";
26
+ type Resultado = {
27
+ ponteiro: Ponteiro;
28
+ } & ({
29
+ tipo: "sem-mudanca";
30
+ } | {
31
+ tipo: "sincronizado";
32
+ escritos: string[];
33
+ removidos: string[];
34
+ } | {
35
+ tipo: "editado-a-mao";
36
+ arquivos: string[];
37
+ });
38
+ /**
39
+ * Estado do que este comando escreveu, contra o hash guardado.
40
+ *
41
+ * Duas situacoes diferentes, e confundi-las custa caro: hash diferente e **edicao a
42
+ * mao**, e o comando tem que parar sem escrever; arquivo **ausente** nao e edicao — e
43
+ * trabalho a refazer, e o comando tem que reescrever.
44
+ */
45
+ export declare function confereDisco(raiz: string, arquivos: Record<string, string>): Promise<{
46
+ editados: string[];
47
+ faltando: string[];
48
+ }>;
49
+ /**
50
+ * O `CLAUDE.md` da raiz existe e importa a politica?
51
+ *
52
+ * Sem politica no servico E sem a linha, nao ha o que apontar nem o que avisar — avisar
53
+ * ai seria ruido que ensina a ignorar aviso. Mas com a linha escrita e a politica vazia o
54
+ * import esta pendurado de verdade, e esse e o silencio que a memoria
55
+ * `import-do-claude-md-falha-calado` manda quebrar.
56
+ */
57
+ export declare function confereOPonteiro(raiz: string, temPolitica: boolean): Promise<Ponteiro>;
58
+ export declare function sync(raiz: string): Promise<Resultado>;
59
+ export {};
package/dist/sync.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { pede } from "./api.js";
2
3
  import { dirname, join } from "node:path";
3
4
  import { gravaManifesto, hashDe, leConfigDoRepo, leManifesto, leToken, } from "./config.js";
4
5
  import { LINHA_DE_IMPORT, materializa } from "./materializa.js";
@@ -31,7 +32,7 @@ export async function confereDisco(raiz, arquivos) {
31
32
  }
32
33
  async function buscaBrain(config, token, etag) {
33
34
  const url = `${config.api}/api/v1/artefatos?tenant=${encodeURIComponent(config.tenant)}&projeto=${encodeURIComponent(config.projeto)}`;
34
- const resposta = await fetch(url, {
35
+ const resposta = await pede(url, {
35
36
  headers: {
36
37
  Authorization: `Bearer ${token}`,
37
38
  ...(etag ? { "If-None-Match": etag } : {}),
@@ -57,16 +58,19 @@ async function buscaBrain(config, token, etag) {
57
58
  /**
58
59
  * O `CLAUDE.md` da raiz existe e importa a politica?
59
60
  *
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.
61
+ * Sem politica no servico E sem a linha, nao ha o que apontar nem o que avisar avisar
62
+ * ai seria ruido que ensina a ignorar aviso. Mas com a linha escrita e a politica vazia o
63
+ * import esta pendurado de verdade, e esse e o silencio que a memoria
64
+ * `import-do-claude-md-falha-calado` manda quebrar.
62
65
  */
63
66
  export async function confereOPonteiro(raiz, temPolitica) {
64
- if (!temPolitica)
65
- return "sem-politica";
66
67
  const claudeMd = await leSeExistir(join(raiz, "CLAUDE.md"));
68
+ const temALinha = claudeMd?.includes(LINHA_DE_IMPORT) ?? false;
69
+ if (!temPolitica)
70
+ return temALinha ? "aponta-para-o-vazio" : "sem-politica";
67
71
  if (claudeMd === null)
68
72
  return "sem-claude-md";
69
- return claudeMd.includes(LINHA_DE_IMPORT) ? "ok" : "sem-a-linha";
73
+ return temALinha ? "ok" : "sem-a-linha";
70
74
  }
71
75
  export async function sync(raiz) {
72
76
  const config = await leConfigDoRepo(raiz);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-harness",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
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",
@@ -21,6 +21,15 @@
21
21
  "bin": {
22
22
  "dd-harness": "dist/index.js"
23
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
+ },
24
33
  "files": [
25
34
  "dist"
26
35
  ],