dd-harness 0.1.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,12 +1,15 @@
1
1
  #!/usr/bin/env node
2
+ import { termosDaConsulta } from "./argv.js";
2
3
  import { check, status } from "./check.js";
3
4
  import { leConfigDoRepo, guardaToken } from "./config.js";
4
5
  import { grava } from "./gravar.js";
5
- import { init } from "./init.js";
6
+ import { init, SUGESTAO_MCP } from "./init.js";
6
7
  import { LINHA_DE_IMPORT } from "./materializa.js";
8
+ import { buscaPolitica } from "./politica.js";
7
9
  import { busca } from "./buscar.js";
8
10
  import { arquiva, edita } from "./curar.js";
9
11
  import { criaPasta } from "./pasta.js";
12
+ import { criaProjeto } from "./projeto.js";
10
13
  import { sync } from "./sync.js";
11
14
  /**
12
15
  * `dd-harness` — o cliente que materializa os artefatos no repositorio.
@@ -18,9 +21,12 @@ import { sync } from "./sync.js";
18
21
  */
19
22
  const AJUDA = `dd-harness — materializa política, briefing e Brain no repositório
20
23
 
24
+ dd-harness login --token <token> [--api <url>]
25
+ guarda a credencial desta máquina
26
+ dd-harness projeto <slug> --nome "<nome>" [--tenant <t>] [--api <url>]
27
+ cria o projeto no serviço (antes do init)
21
28
  dd-harness init --tenant <t> --projeto <p> [--api <url>]
22
29
  prepara o repositório (config + CLAUDE.md)
23
- dd-harness login --token <token> guarda a credencial desta máquina
24
30
  dd-harness pasta <slug> --definicao "o que entra e o que não entra"
25
31
  cria a pasta que o gravar exige
26
32
  dd-harness gravar <arquivo.md> registra uma memória a partir de um markdown
@@ -31,7 +37,10 @@ const AJUDA = `dd-harness — materializa política, briefing e Brain no reposit
31
37
  dd-harness buscar "<pergunta>" acha memória por relevância, não por arquivo
32
38
  dd-harness sync escreve os artefatos em disco
33
39
  dd-harness check [--commit <sha>] mede as âncoras e reporta a deriva
34
- dd-harness status só lê: o que espera julgamento
40
+ dd-harness status só lê: o tamanho do Brain e o que espera julgamento
41
+ dd-harness politica imprime a política do serviço (para o hook de sessão)
42
+ saída 0 = veio; 3 = projeto sem política;
43
+ 1 = não consegui buscar
35
44
  dd-harness --help
36
45
 
37
46
  O gerado vive em dd-harness/. Na raiz fica só o seu CLAUDE.md, que importa a
@@ -106,14 +115,30 @@ async function comandoInit(argv) {
106
115
  "ja-tinha-a-linha": "mantido CLAUDE.md — já importava a política",
107
116
  }[r.claudeMd]);
108
117
  console.log("\nAgora: dd-harness login --token <token> && dd-harness sync");
118
+ if (r.mcp === "ja-declarado") {
119
+ console.log("\nmantido .mcp.json — o servidor dd-harness já está declarado");
120
+ return;
121
+ }
122
+ console.log("\nOpcional: as memórias como ferramenta, para o agente buscar e gravar sem" +
123
+ "\nescrever arquivo. Acrescente ao `.mcp.json` da raiz (não escrevo nele: o" +
124
+ "\narquivo é seu e pode declarar outros servidores):\n");
125
+ console.log(SUGESTAO_MCP);
109
126
  }
127
+ /**
128
+ * Nao exige `.dd-harness.json`, e isso importa: sem projeto no servico o `init` nao tem o
129
+ * que apontar, criar projeto exige credencial, e credencial exigindo config fechava um
130
+ * ciclo sem entrada — no dia um de um repositorio novo, nenhum dos tres rodava. O `--api`
131
+ * resolve; com config no repo, ela preenche.
132
+ */
110
133
  async function comandoLogin(argv) {
111
134
  const token = argumento(argv, "token");
112
135
  if (!token)
113
- throw new Error("uso: dd-harness login --token <token>");
114
- const config = await leConfigDoRepo(process.cwd());
115
- const caminho = await guardaToken(config.api, token);
116
- console.log(`credencial de ${config.api} guardada em ${caminho}`);
136
+ throw new Error("uso: dd-harness login --token <token> [--api <url>]");
137
+ const daLinha = argumento(argv, "api");
138
+ const doRepo = daLinha ? null : await leConfigDoRepo(process.cwd()).catch(() => null);
139
+ const api = (daLinha ?? doRepo?.api ?? "https://dd-harness.vercel.app").replace(/\/$/, "");
140
+ const caminho = await guardaToken(api, token);
141
+ console.log(`credencial de ${api} guardada em ${caminho}`);
117
142
  }
118
143
  async function comandoEditar(argv) {
119
144
  const caminho = argv[0];
@@ -144,7 +169,7 @@ async function comandoArquivar(argv) {
144
169
  console.log(" foi para o histórico, não foi apagada. Rode `dd-harness sync`.");
145
170
  }
146
171
  async function comandoBuscar(argv) {
147
- const consulta = argv.filter((a) => !a.startsWith("--")).join(" ").trim();
172
+ const consulta = termosDaConsulta(argv);
148
173
  if (!consulta)
149
174
  throw new Error('uso: dd-harness buscar "<pergunta>"');
150
175
  const limite = Number(argumento(argv, "limite")) || undefined;
@@ -173,8 +198,11 @@ async function comandoSync() {
173
198
  "parei sem escrever nada: estes arquivos foram editados à mão.",
174
199
  ...resultado.arquivos.map((a) => ` ${a}`),
175
200
  "",
176
- "O disco é projeção do serviço, uma direção só. Leve a mudança para o serviço,",
177
- "ou descarte a edição local (git checkout / apague o arquivo) e sincronize de novo.",
201
+ "O disco é projeção do serviço, uma direção só. Duas saídas:",
202
+ "",
203
+ " 1. Leve a edição para o serviço — `dd-harness editar <arquivo>` para cada um",
204
+ " acima. É o caminho normal de corrigir memória, e destrava o sync.",
205
+ " 2. Descarte a edição local (git checkout / apague o arquivo) e sincronize.",
178
206
  ].join("\n"));
179
207
  process.exitCode = 1;
180
208
  return;
@@ -238,12 +266,55 @@ async function comandoCheck(argv) {
238
266
  }
239
267
  }
240
268
  }
269
+ /**
270
+ * Imprime a politica no stdout, para o hook `SessionStart` injetar no contexto.
271
+ *
272
+ * Os codigos de saida sao o contrato com o hook, e existem para separar duas coisas que
273
+ * um "falhou" generico colapsaria: projeto novo (que precisa de briefing) de politica
274
+ * inalcancavel (que precisa PARAR a sessao). Mudar estes numeros quebra o hook.
275
+ */
276
+ async function comandoPolitica() {
277
+ const r = await buscaPolitica(process.cwd());
278
+ if (r.estado === "ok") {
279
+ console.log(r.conteudo);
280
+ return;
281
+ }
282
+ if (r.estado === "sem-politica") {
283
+ console.error("Este projeto ainda não tem política — nunca foi briefado. Rode `/briefar`.");
284
+ process.exitCode = 3;
285
+ return;
286
+ }
287
+ console.error(`não consegui buscar a política: ${r.motivo}`);
288
+ process.exitCode = 1;
289
+ }
241
290
  async function comandoStatus() {
242
291
  const r = await status(process.cwd());
292
+ // O acervo vem primeiro, e sempre: e a resposta para "o que ha no Brain deste projeto?",
293
+ // que era a pergunta sem comando. Sem ela, quem quer saber conta arquivo em disco — e
294
+ // acerta por acidente, porque memoria arquivada sai do disco e continua no indice.
295
+ const { ativas, arquivadas, porPasta } = r.acervo;
296
+ if (ativas === 0 && arquivadas === 0) {
297
+ console.log("dd-harness: o Brain deste projeto está vazio.");
298
+ }
299
+ else {
300
+ const detalhe = porPasta.map((p) => `${p.pasta} ${p.quantas}`).join(", ");
301
+ console.log(`dd-harness: ${ativas} memória(s) ativa(s)` +
302
+ (arquivadas ? ` e ${arquivadas} arquivada(s)` : "") +
303
+ (detalhe ? ` — ${detalhe}` : ""));
304
+ }
305
+ // Antes do early return abaixo: fila travada nao e "julgamento esperando", e sumiria
306
+ // justamente na sessao mais comum — a que nao tem deriva nem vencida.
307
+ if (r.travadasNaFila > 0) {
308
+ console.log("");
309
+ console.log(`AVISO: ${r.travadasNaFila} memória(s) desistiram de ser indexadas e não entram na busca semântica.`);
310
+ console.log(" O indexador tentou 5 vezes e parou. Rode `pnpm worker --reindexar`;");
311
+ console.log(" se repetir, o motivo está em `embedding_queue.ultimo_erro`.");
312
+ }
243
313
  if (!r.comDeriva.length && !r.vencidas.length) {
244
- console.log("dd-harness: nada esperando julgamento.");
314
+ console.log("Nada esperando julgamento.");
245
315
  return;
246
316
  }
317
+ console.log("");
247
318
  if (r.comDeriva.length) {
248
319
  const total = r.comDeriva.reduce((soma, m) => soma + m.abertas, 0);
249
320
  console.log(`dd-harness: ${total} deriva(s) aberta(s), esperando julgamento:`);
@@ -258,6 +329,25 @@ async function comandoStatus() {
258
329
  console.log(` ${m.pasta}/${m.memoria} — ${m.titulo}`);
259
330
  }
260
331
  }
332
+ /**
333
+ * Vem ANTES do `init`, e nao depois: o `init` escreve o `.dd-harness.json` apontando para
334
+ * um projeto, e apontar para projeto que nao existe deixaria todo comando seguinte em 404.
335
+ */
336
+ async function comandoProjeto(argv) {
337
+ const slug = argv[0];
338
+ const nome = argumento(argv, "nome");
339
+ if (!slug || slug.startsWith("-") || !nome) {
340
+ throw new Error('uso: dd-harness projeto <slug> --nome "<nome>" [--tenant <slug>] [--api <url>]');
341
+ }
342
+ const r = await criaProjeto(process.cwd(), slug, nome, {
343
+ tenant: argumento(argv, "tenant"),
344
+ api: argumento(argv, "api"),
345
+ });
346
+ console.log(r.jaExistia
347
+ ? `projeto ${r.projeto} já existia — nada criado.`
348
+ : `criado projeto ${r.projeto}.`);
349
+ console.log(`Agora: dd-harness init --tenant <espaço> --projeto ${r.projeto}`);
350
+ }
261
351
  async function comandoPasta(argv) {
262
352
  const slug = argv[0];
263
353
  const definicao = argumento(argv, "definicao");
@@ -292,6 +382,8 @@ async function principal() {
292
382
  return comandoInit(resto);
293
383
  case "login":
294
384
  return comandoLogin(resto);
385
+ case "projeto":
386
+ return comandoProjeto(resto);
295
387
  case "pasta":
296
388
  return comandoPasta(resto);
297
389
  case "gravar":
@@ -308,6 +400,8 @@ async function principal() {
308
400
  return comandoCheck(resto);
309
401
  case "status":
310
402
  return comandoStatus();
403
+ case "politica":
404
+ return comandoPolitica();
311
405
  case "--help":
312
406
  case "-h":
313
407
  case undefined:
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,70 @@
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
+ * Memorias que excederam as tentativas de indexacao e nao serao mais tentadas. Ficam
49
+ * sem embedding — somem da busca semantica — e so este numero denuncia.
50
+ */
51
+ travadas_na_fila?: number;
52
+ };
53
+ export declare function arquivoDaMemoria(m: Memoria): string;
54
+ export declare function arquivoDoIndice(brain: Brain): string;
55
+ /** Tudo o que o servico gera vive aqui — e nada fora daqui e escrito pelo `sync`. */
56
+ export declare const PASTA = "dd-harness";
57
+ /** O que a raiz precisa conter para a politica chegar a sessao. */
58
+ export declare const LINHA_DE_IMPORT = "@dd-harness/politica.md";
59
+ /**
60
+ * Caminho relativo (POSIX) -> conteúdo. As chaves são o que o manifesto guarda.
61
+ *
62
+ * Tudo dentro de `dd-harness/`, inclusive a política. Na raiz fica só o `CLAUDE.md`, que
63
+ * é **seu**: o `sync` não o escreve, apenas confere que ele importa a política. É o que
64
+ * deixa conviverem a parte gerenciada e o que aquele repositório tem de próprio — e o
65
+ * que faz adotar um projeto existente ser uma linha, não um ritual.
66
+ *
67
+ * Artefato vazio ou ausente não entra no mapa; como o manifesto remove o que saiu do
68
+ * conjunto, esvaziar no serviço apaga o arquivo no próximo `sync`.
69
+ */
70
+ 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)}`] : []),
@@ -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 CHANGED
@@ -1,4 +1,4 @@
1
- import { leConfigDoRepo, leToken } from "./config.js";
1
+ import { cabecalhos, credencial, pede, recusa } from "./api.js";
2
2
  /**
3
3
  * `dd-harness pasta <slug> --definicao "..."` — cria a pasta sem sair da sessao.
4
4
  *
@@ -7,14 +7,10 @@ import { leConfigDoRepo, leToken } from "./config.js";
7
7
  * o agente parava e pedia que alguem abrisse o navegador.
8
8
  */
9
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`, {
10
+ const { config, token } = await credencial(raiz);
11
+ const resposta = await pede(`${config.api}/api/v1/pastas`, {
16
12
  method: "POST",
17
- headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
13
+ headers: cabecalhos(token, true),
18
14
  body: JSON.stringify({
19
15
  tenant: config.tenant,
20
16
  projeto: config.projeto,
@@ -27,10 +23,8 @@ export async function criaPasta(raiz, slug, definicao) {
27
23
  // nao criou nada.
28
24
  if (resposta.status === 409)
29
25
  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
- }
26
+ if (!resposta.ok)
27
+ await recusa(resposta);
34
28
  const { pasta } = (await resposta.json());
35
29
  return { pasta, jaExistia: false };
36
30
  }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * A politica do projeto, buscada no servico na hora.
3
+ *
4
+ * Existe para o hook `SessionStart`: sem materializacao em disco, e ele quem garante que
5
+ * nenhuma sessao abre sem protocolo. Por isso o retorno separa TRES situacoes que um
6
+ * "deu erro" colapsaria — e cada uma pede uma reacao diferente de quem chamou:
7
+ *
8
+ * - `ok`: a politica existe e veio. Segue a sessao.
9
+ * - `sem-politica`: o projeto existe e nunca foi briefado. NAO e falha: a saida e rodar o
10
+ * briefing. Tratar como erro faria todo projeto novo parecer quebrado.
11
+ * - `inalcancavel`: rede, timeout, 5xx, credencial, projeto inexistente. A politica pode
12
+ * existir e nao chegou — e ai a sessao precisa PARAR, porque seguir sem ela e
13
+ * exatamente a falha que este projeto combate.
14
+ *
15
+ * A diferenca entre as duas ultimas e o que o `GET /api/v1/artefatos` responde: `404` e
16
+ * "nao ha projeto"; `200` com `politica` nula e "existe, nunca briefado".
17
+ */
18
+ export type ResultadoDaPolitica = {
19
+ estado: "ok";
20
+ conteudo: string;
21
+ } | {
22
+ estado: "sem-politica";
23
+ } | {
24
+ estado: "inalcancavel";
25
+ motivo: string;
26
+ };
27
+ export declare function buscaPolitica(raiz: string): Promise<ResultadoDaPolitica>;
@@ -0,0 +1,35 @@
1
+ import { cabecalhos, credencial, pede } from "./api.js";
2
+ export async function buscaPolitica(raiz) {
3
+ let config;
4
+ try {
5
+ config = await credencial(raiz);
6
+ }
7
+ catch (erro) {
8
+ // Sem `.dd-harness.json` ou sem token nao da para nem perguntar. E inalcancavel, nao
9
+ // "sem politica": a politica pode muito bem existir do outro lado.
10
+ return { estado: "inalcancavel", motivo: mensagem(erro) };
11
+ }
12
+ const url = new URL(`${config.config.api}/api/v1/artefatos`);
13
+ url.searchParams.set("tenant", config.config.tenant);
14
+ url.searchParams.set("projeto", config.config.projeto);
15
+ try {
16
+ const resposta = await pede(url, { headers: cabecalhos(config.token) });
17
+ if (!resposta.ok) {
18
+ const { erro } = (await resposta.json().catch(() => ({})));
19
+ return {
20
+ estado: "inalcancavel",
21
+ motivo: erro ?? `a API respondeu ${resposta.status}.`,
22
+ };
23
+ }
24
+ const payload = (await resposta.json());
25
+ const conteudo = payload.politica?.trim();
26
+ // Vazio e nulo sao a mesma coisa aqui, e os dois significam "nunca foi escrita".
27
+ if (!conteudo)
28
+ return { estado: "sem-politica" };
29
+ return { estado: "ok", conteudo };
30
+ }
31
+ catch (erro) {
32
+ return { estado: "inalcancavel", motivo: mensagem(erro) };
33
+ }
34
+ }
35
+ const mensagem = (erro) => erro instanceof Error ? erro.message : String(erro);
@@ -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 } : {}),