dd-harness 0.38.2 → 0.40.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,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { diagnostico } from "./diagnostico.js";
3
- import { aplicaAtualizacao, diagnosticaAtualizacao, versaoInstalada, versaoPublicada } from "./atualizar.js";
3
+ import { aplicaAtualizacao, diagnosticaAtualizacao, reescrevePonteiros, versaoInstalada, versaoPublicada } from "./atualizar.js";
4
4
  import { instalaHosts, selecionaHosts } from "./hosts.js";
5
5
  import { achaRaiz } from "./config.js";
6
6
  import { abreSessao, guardaSessao, saidaDaGuarda, saidaDoBoot } from "./sessao.js";
@@ -27,7 +27,7 @@ import { pergunta, escolha, fechaPerguntas } from "./pergunta.js";
27
27
  import { buscaPolitica } from "./politica.js";
28
28
  import { avisoDeOrdem, blocoDeSessao, criaFase, editaFase, formataChangelog, formataRoadmap, leFases, } from "./roadmap.js";
29
29
  import { decideDoHook, guardaCache } from "./cinto.js";
30
- import { escrevePonteirosDeSkills, leSkill, leSkills, semeiaSkills } from "./skill.js";
30
+ import { criaSkill, editaSkill, escrevePonteirosDeSkills, leSkill, leSkills, semeiaSkills } from "./skill.js";
31
31
  import { SKILLS_INICIAIS } from "./skills-iniciais.js";
32
32
  import { busca } from "./buscar.js";
33
33
  import { apaga, arquiva, edita, le, promove } from "./curar.js";
@@ -80,7 +80,7 @@ const AJUDA = `dd-harness — a política e o Brain do projeto, no serviço
80
80
  projeto do espaço, inclusive os futuros
81
81
  dd-harness despromover <pasta>/<slug>
82
82
  traz de volta ao alcance dos vínculos
83
- dd-harness apagar <pasta>/<slug> [--confirmar <espaço>]
83
+ dd-harness apagar <pasta>/<slug> [--confirmar "<frase devolvida>"]
84
84
  apaga de vez, em cascata — sem desfazer.
85
85
  Para tirar de circulação guardando o
86
86
  conteúdo, use "arquivar"
@@ -101,6 +101,11 @@ const AJUDA = `dd-harness — a política e o Brain do projeto, no serviço
101
101
  dd-harness skills as skills deste projeto e quando cada uma serve
102
102
  dd-harness skill <nome> o procedimento de uma delas (o mesmo que o
103
103
  agente recebe ao invocá-la)
104
+ dd-harness skill criar <nome> --descricao "<quando invocar>" [--conteudo <arquivo.md>]
105
+ [--so-por-comando true|false]
106
+ dd-harness skill editar <nome> [--descricao "<d>"] [--conteudo <arquivo.md>]
107
+ [--so-por-comando true|false]
108
+ grava no serviço e reescreve o ponteiro em disco
104
109
  dd-harness roadmap as fases abertas: a atual inteira, as próximas
105
110
  por título (opcional — projeto sem fase não tem)
106
111
  dd-harness changelog [--versao <v>]
@@ -140,11 +145,9 @@ function argumento(argv, nome) {
140
145
  const API_PADRAO = "https://dd-harness.vercel.app";
141
146
  // --- roadmap e changelog ---
142
147
  /**
143
- * `skills` e `skill <slug>` — so LEITURA.
144
- *
145
- * Criar e editar ficam na interface de proposito: escrever procedimento e trabalho de
146
- * texto longo, e um editor de verdade e melhor que um flag de linha de comando. O que o
147
- * terminal precisa e ver o que existe e conferir um procedimento sem trocar de janela.
148
+ * `skills` e `skill <slug>` — leitura. Criar e editar sao `skill criar|editar`, com o
149
+ * procedimento vindo de um .md (`--conteudo`), como em `fase` e `gravar`: sem isso a skill
150
+ * `escrever-skill` redigia o texto e nao tinha como grava-lo fora da interface.
148
151
  */
149
152
  async function comandoSkills() {
150
153
  const skills = await leSkills(process.cwd());
@@ -160,10 +163,44 @@ async function comandoSkills() {
160
163
  }
161
164
  console.log(`${skills.length} skill(s). \`dd-harness skill <nome>\` mostra o procedimento.`);
162
165
  }
166
+ /** `--so-por-comando true|false`: flag solta seria so "ligar", e desligar tambem e edicao. */
167
+ function soPorComandoDoArgv(argv) {
168
+ const v = argumento(argv, "so-por-comando");
169
+ if (v === undefined)
170
+ return undefined;
171
+ if (v !== "true" && v !== "false")
172
+ throw new Error("--so-por-comando: use true ou false.");
173
+ return v === "true";
174
+ }
175
+ /**
176
+ * `skill criar` e `skill editar`. O ponteiro em disco e reescrito em seguida: descricao e
177
+ * "so por comando" moram no frontmatter dele, e o host so le o disco.
178
+ */
179
+ async function comandoSkillEscrita(acao, argv) {
180
+ const [slug, ...opcoes] = argv;
181
+ if (!slug || slug.startsWith("--"))
182
+ throw new Error(`skill ${acao} exige o nome da skill.`);
183
+ const campos = {
184
+ descricao: argumento(opcoes, "descricao"),
185
+ conteudo: await conteudoDoArgv(opcoes),
186
+ so_por_comando: soPorComandoDoArgv(opcoes),
187
+ };
188
+ if (acao === "criar") {
189
+ if (!campos.descricao)
190
+ throw new Error('skill criar exige --descricao "<quando invocar>".');
191
+ await criaSkill(process.cwd(), { ...campos, slug, descricao: campos.descricao, conteudo: campos.conteudo ?? "" });
192
+ }
193
+ else {
194
+ await editaSkill(process.cwd(), slug, campos);
195
+ }
196
+ console.log(`Skill ${slug} ${acao === "criar" ? "criada" : "atualizada"}; ${await reescrevePonteiros(process.cwd())}.`);
197
+ }
163
198
  async function comandoSkill(argv) {
164
199
  const slug = argv[0];
200
+ if (slug === "criar" || slug === "editar")
201
+ return comandoSkillEscrita(slug, argv.slice(1));
165
202
  if (!slug || slug.startsWith("-")) {
166
- throw new Error("uso: dd-harness skill <nome>");
203
+ throw new Error("uso: dd-harness skill <nome> | dd-harness skill criar|editar <nome> [...]");
167
204
  }
168
205
  const s = await leSkill(process.cwd(), slug);
169
206
  console.log(`# ${s.slug}
@@ -691,21 +728,21 @@ async function comandoPromover(argv, global) {
691
728
  /**
692
729
  * `apagar` — o unico caminho que destroi.
693
730
  *
694
- * Duas etapas, e a segunda pede o nome do espaco digitado. Nao e cerimonia: a cascata leva
731
+ * Duas etapas, e a segunda pede a frase vinculada à versão atual. Nao e cerimonia: a cascata leva
695
732
  * ancoras e toda a deriva medida delas, e nao ha desfazer. Arquivar continua sendo o
696
733
  * caminho normal — isto e para o que nunca deveria ter existido.
697
734
  */
698
735
  async function comandoApagar(argv) {
699
736
  const endereco = argv[0];
700
737
  if (!endereco || endereco.startsWith("-") || !endereco.includes("/")) {
701
- throw new Error("uso: dd-harness apagar <pasta>/<slug> [--confirmar <espaço>]");
738
+ throw new Error('uso: dd-harness apagar <pasta>/<slug> [--confirmar "<frase devolvida>"]');
702
739
  }
703
740
  const r = await apaga(process.cwd(), endereco, argumento(argv, "confirmar"));
704
741
  if (!r.apagou) {
705
742
  console.log(r.detalhe);
706
743
  if (r.confirmacaoEsperada) {
707
744
  console.log(`
708
- dd-harness apagar ${endereco} --confirmar ${r.confirmacaoEsperada}`);
745
+ dd-harness apagar ${endereco} --confirmar "${r.confirmacaoEsperada}"`);
709
746
  }
710
747
  // Sem exit diferente de zero: recusar por falta de confirmacao nao e falha, e o
711
748
  // caminho normal da primeira chamada.
package/dist/init.js CHANGED
@@ -8,13 +8,13 @@ import { CAMINHO_CONFIG } from "./config.js";
8
8
  * onde falha silenciosa nasce — entrada errada nao da erro, a ferramenta so nao aparece.
9
9
  * Quem cola sabe o que colou.
10
10
  */
11
- export const SUGESTAO_MCP = `{
12
- "mcpServers": {
13
- "dd-harness": {
14
- "command": "npx",
15
- "args": ["-y", "dd-harness-mcp"]
16
- }
17
- }
11
+ export const SUGESTAO_MCP = `{
12
+ "mcpServers": {
13
+ "dd-harness": {
14
+ "command": "npx",
15
+ "args": ["-y", "dd-harness-mcp"]
16
+ }
17
+ }
18
18
  }`;
19
19
  /**
20
20
  * Os dois hooks do dd-harness, pelas duas razoes que nenhuma instrucao em markdown
@@ -33,31 +33,31 @@ export const SUGESTAO_MCP = `{
33
33
  *
34
34
  * Sugerido e nao escrito, pelo mesmo motivo do `.mcp.json`.
35
35
  */
36
- export const SUGESTAO_HOOK = `{
37
- "hooks": {
38
- "SessionStart": [
39
- {
40
- "hooks": [
41
- {
42
- "type": "command",
43
- "command": "dd-harness politica --hook",
44
- "statusMessage": "Carregando a política do dd-harness..."
45
- }
46
- ]
47
- }
48
- ],
49
- "PreToolUse": [
50
- {
51
- "matcher": "Edit|Write|MultiEdit",
52
- "hooks": [
53
- {
54
- "type": "command",
55
- "command": "dd-harness cinto"
56
- }
57
- ]
58
- }
59
- ]
60
- }
36
+ export const SUGESTAO_HOOK = `{
37
+ "hooks": {
38
+ "SessionStart": [
39
+ {
40
+ "hooks": [
41
+ {
42
+ "type": "command",
43
+ "command": "dd-harness politica --hook",
44
+ "statusMessage": "Carregando a política do dd-harness..."
45
+ }
46
+ ]
47
+ }
48
+ ],
49
+ "PreToolUse": [
50
+ {
51
+ "matcher": "Edit|Write|MultiEdit",
52
+ "hooks": [
53
+ {
54
+ "type": "command",
55
+ "command": "dd-harness cinto"
56
+ }
57
+ ]
58
+ }
59
+ ]
60
+ }
61
61
  }`;
62
62
  /**
63
63
  * O que escrever num `AGENTS.md`, para agente que NAO e o Claude Code.
@@ -71,19 +71,19 @@ export const SUGESTAO_HOOK = `{
71
71
  * simplesmente nunca perguntar pela politica. Dai esta linha, que e curta de proposito —
72
72
  * ela manda buscar a regra, nao repete a regra.
73
73
  */
74
- export const SUGESTAO_AGENTS = `# AGENTS.md
75
-
76
- Este projeto tem um protocolo de trabalho que **não está neste arquivo**.
77
-
78
- **ANTES de qualquer outra coisa**, chame a ferramenta MCP \`ler_artefato\` com
79
- \`tipo: "politica"\` e siga o que ela devolver. É a primeira ação da sessão, antes
80
- de ler código, responder ou planejar.
81
-
82
- - Se a ferramenta não existir, o servidor MCP do dd-harness não está declarado:
83
- avise o usuário e **não modifique nada** até ele resolver.
84
- - Se ela devolver vazio, este projeto nunca foi briefado — diga isso ao usuário.
85
-
86
- O Claude Code carrega a política sozinho, por hook. Nas outras ferramentas, a
74
+ export const SUGESTAO_AGENTS = `# AGENTS.md
75
+
76
+ Este projeto tem um protocolo de trabalho que **não está neste arquivo**.
77
+
78
+ **ANTES de qualquer outra coisa**, chame a ferramenta MCP \`ler_artefato\` com
79
+ \`tipo: "politica"\` e siga o que ela devolver. É a primeira ação da sessão, antes
80
+ de ler código, responder ou planejar.
81
+
82
+ - Se a ferramenta não existir, o servidor MCP do dd-harness não está declarado:
83
+ avise o usuário e **não modifique nada** até ele resolver.
84
+ - Se ela devolver vazio, este projeto nunca foi briefado — diga isso ao usuário.
85
+
86
+ O Claude Code carrega a política sozinho, por hook. Nas outras ferramentas, a
87
87
  chamada acima é o que substitui esse hook.`;
88
88
  async function declaraMcp(raiz) {
89
89
  try {
@@ -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)}`] : []),
@@ -10,81 +10,81 @@
10
10
  */
11
11
  export const MOLDES_HISTORICOS = [
12
12
  // ca1e529
13
- `## Protocolo do dd-harness
14
-
15
- Este projeto tem um protocolo de trabalho que **não está neste arquivo**.
16
-
17
- **ANTES de qualquer outra coisa**, chame a ferramenta MCP \`ler_artefato\` com
18
- \`tipo: "politica"\` e siga o que ela devolver. É a primeira ação da sessão, antes
19
- de ler código, responder ou planejar.
20
-
21
- - Se a ferramenta não existir, o servidor MCP do dd-harness não está declarado:
22
- avise o usuário e **não modifique nada** até ele resolver.
23
- - Se ela devolver vazio, este projeto nunca foi briefado — diga isso ao usuário.
24
-
25
- Leia também o briefing com a mesma ferramenta (tipo briefing). Os dois são obrigatórios.
26
-
27
- Depois, \`ler_roadmap\`: se houver uma fase **Agora**, é dela que saem os passos
28
- desta sessão. Lista vazia significa que este projeto não usa roadmap, e isso é
29
- válido — não crie fase sem o usuário pedir.
30
-
31
- E \`listar_skills\`: são os procedimentos deste projeto. **Invoque a que couber
32
- ANTES de fazer o trabalho, não depois** — skill lida no fim vira revisão do que
33
- já saiu errado, e é tarde.
34
-
35
- Vale mesmo quando o pedido parece pequeno: "é só um ajuste" é exatamente a
36
- frase que antecede pular o procedimento. A política diz quais são obrigatórias
13
+ `## Protocolo do dd-harness
14
+
15
+ Este projeto tem um protocolo de trabalho que **não está neste arquivo**.
16
+
17
+ **ANTES de qualquer outra coisa**, chame a ferramenta MCP \`ler_artefato\` com
18
+ \`tipo: "politica"\` e siga o que ela devolver. É a primeira ação da sessão, antes
19
+ de ler código, responder ou planejar.
20
+
21
+ - Se a ferramenta não existir, o servidor MCP do dd-harness não está declarado:
22
+ avise o usuário e **não modifique nada** até ele resolver.
23
+ - Se ela devolver vazio, este projeto nunca foi briefado — diga isso ao usuário.
24
+
25
+ Leia também o briefing com a mesma ferramenta (tipo briefing). Os dois são obrigatórios.
26
+
27
+ Depois, \`ler_roadmap\`: se houver uma fase **Agora**, é dela que saem os passos
28
+ desta sessão. Lista vazia significa que este projeto não usa roadmap, e isso é
29
+ válido — não crie fase sem o usuário pedir.
30
+
31
+ E \`listar_skills\`: são os procedimentos deste projeto. **Invoque a que couber
32
+ ANTES de fazer o trabalho, não depois** — skill lida no fim vira revisão do que
33
+ já saiu errado, e é tarde.
34
+
35
+ Vale mesmo quando o pedido parece pequeno: "é só um ajuste" é exatamente a
36
+ frase que antecede pular o procedimento. A política diz quais são obrigatórias
37
37
  e quando.`,
38
38
  // 875342e
39
- `## Protocolo do dd-harness
40
-
41
- Este projeto tem um protocolo de trabalho que **não está neste arquivo**.
42
-
43
- **ANTES de qualquer outra coisa**, chame a ferramenta MCP \`ler_artefato\` com
44
- \`tipo: "politica"\` e siga o que ela devolver. É a primeira ação da sessão, antes
45
- de ler código, responder ou planejar.
46
-
47
- - Se a ferramenta não existir, o servidor MCP do dd-harness não está declarado:
48
- avise o usuário e **não modifique nada** até ele resolver.
49
- - Se ela devolver vazio, este projeto nunca foi briefado — diga isso ao usuário.
50
-
51
- Depois, \`ler_roadmap\`: se houver uma fase **Agora**, é dela que saem os passos
52
- desta sessão. Lista vazia significa que este projeto não usa roadmap, e isso é
53
- válido — não crie fase sem o usuário pedir.
54
-
55
- E \`listar_skills\`: são os procedimentos deste projeto. **Invoque a que couber
56
- ANTES de fazer o trabalho, não depois** — skill lida no fim vira revisão do que
57
- já saiu errado, e é tarde.
58
-
59
- Vale mesmo quando o pedido parece pequeno: "é só um ajuste" é exatamente a
60
- frase que antecede pular o procedimento. A política diz quais são obrigatórias
39
+ `## Protocolo do dd-harness
40
+
41
+ Este projeto tem um protocolo de trabalho que **não está neste arquivo**.
42
+
43
+ **ANTES de qualquer outra coisa**, chame a ferramenta MCP \`ler_artefato\` com
44
+ \`tipo: "politica"\` e siga o que ela devolver. É a primeira ação da sessão, antes
45
+ de ler código, responder ou planejar.
46
+
47
+ - Se a ferramenta não existir, o servidor MCP do dd-harness não está declarado:
48
+ avise o usuário e **não modifique nada** até ele resolver.
49
+ - Se ela devolver vazio, este projeto nunca foi briefado — diga isso ao usuário.
50
+
51
+ Depois, \`ler_roadmap\`: se houver uma fase **Agora**, é dela que saem os passos
52
+ desta sessão. Lista vazia significa que este projeto não usa roadmap, e isso é
53
+ válido — não crie fase sem o usuário pedir.
54
+
55
+ E \`listar_skills\`: são os procedimentos deste projeto. **Invoque a que couber
56
+ ANTES de fazer o trabalho, não depois** — skill lida no fim vira revisão do que
57
+ já saiu errado, e é tarde.
58
+
59
+ Vale mesmo quando o pedido parece pequeno: "é só um ajuste" é exatamente a
60
+ frase que antecede pular o procedimento. A política diz quais são obrigatórias
61
61
  e quando.`,
62
62
  // f59da48
63
- `## Protocolo do dd-harness
64
-
65
- Este projeto tem um protocolo de trabalho que **não está neste arquivo**.
66
-
67
- **ANTES de qualquer outra coisa**, chame a ferramenta MCP \`ler_artefato\` com
68
- \`tipo: "politica"\` e siga o que ela devolver. É a primeira ação da sessão, antes
69
- de ler código, responder ou planejar.
70
-
71
- - Se a ferramenta não existir, o servidor MCP do dd-harness não está declarado:
72
- avise o usuário e **não modifique nada** até ele resolver.
73
- - Se ela devolver vazio, este projeto nunca foi briefado — diga isso ao usuário.
74
-
75
- Depois, \`ler_roadmap\`: se houver uma fase **Agora**, é dela que saem os passos
76
- desta sessão. Lista vazia significa que este projeto não usa roadmap, e isso é
63
+ `## Protocolo do dd-harness
64
+
65
+ Este projeto tem um protocolo de trabalho que **não está neste arquivo**.
66
+
67
+ **ANTES de qualquer outra coisa**, chame a ferramenta MCP \`ler_artefato\` com
68
+ \`tipo: "politica"\` e siga o que ela devolver. É a primeira ação da sessão, antes
69
+ de ler código, responder ou planejar.
70
+
71
+ - Se a ferramenta não existir, o servidor MCP do dd-harness não está declarado:
72
+ avise o usuário e **não modifique nada** até ele resolver.
73
+ - Se ela devolver vazio, este projeto nunca foi briefado — diga isso ao usuário.
74
+
75
+ Depois, \`ler_roadmap\`: se houver uma fase **Agora**, é dela que saem os passos
76
+ desta sessão. Lista vazia significa que este projeto não usa roadmap, e isso é
77
77
  válido — não crie fase sem o usuário pedir.`,
78
78
  // 5fb3f02
79
- `## Protocolo do dd-harness
80
-
81
- Este projeto tem um protocolo de trabalho que **não está neste arquivo**.
82
-
83
- **ANTES de qualquer outra coisa**, chame a ferramenta MCP \`ler_artefato\` com
84
- \`tipo: "politica"\` e siga o que ela devolver. É a primeira ação da sessão, antes
85
- de ler código, responder ou planejar.
86
-
87
- - Se a ferramenta não existir, o servidor MCP do dd-harness não está declarado:
88
- avise o usuário e **não modifique nada** até ele resolver.
79
+ `## Protocolo do dd-harness
80
+
81
+ Este projeto tem um protocolo de trabalho que **não está neste arquivo**.
82
+
83
+ **ANTES de qualquer outra coisa**, chame a ferramenta MCP \`ler_artefato\` com
84
+ \`tipo: "politica"\` e siga o que ela devolver. É a primeira ação da sessão, antes
85
+ de ler código, responder ou planejar.
86
+
87
+ - Se a ferramenta não existir, o servidor MCP do dd-harness não está declarado:
88
+ avise o usuário e **não modifique nada** até ele resolver.
89
89
  - Se ela devolver vazio, este projeto nunca foi briefado — diga isso ao usuário.`,
90
90
  ];
@@ -0,0 +1,31 @@
1
+ /**
2
+ * O payload de sessao guardado em disco, com o ETag que o servico deu.
3
+ *
4
+ * `GET /api/v1/artefatos` ja respondia `ETag` e ja sabia responder `304` — mas nenhum
5
+ * cliente mandava `If-None-Match`, entao o caminho condicional nunca rodava: toda sessao
6
+ * baixava o Brain inteiro de novo, igual ao acervo inteiro, mesmo quando nada mudara.
7
+ *
8
+ * O cache guarda o corpo junto do ETag porque `304` vem SEM corpo — sem a copia local,
9
+ * responder 304 deixaria quem chamou sem payload nenhum. O disco e otimizacao, nao
10
+ * contrato: qualquer falha de leitura ou escrita cai para a requisicao completa, que e o
11
+ * comportamento de antes.
12
+ */
13
+ export type PayloadCacheado = {
14
+ etag: string;
15
+ corpo: string;
16
+ projeto?: string;
17
+ };
18
+ /** Sem cache, cache ilegivel, de formato antigo ou de outro projeto: `null` — pede inteiro. */
19
+ export declare function leCacheDoPayload(raiz: string): Promise<PayloadCacheado | null>;
20
+ /** Falha em silencio: cache e otimizacao. Nao vale derrubar a sessao por disco cheio. */
21
+ export declare function guardaCacheDoPayload(raiz: string, cache: PayloadCacheado): Promise<void>;
22
+ /**
23
+ * O corpo da resposta, vindo da rede ou do cache — e o cache ja atualizado.
24
+ *
25
+ * Trata as tres situacoes que interessam a quem chama:
26
+ * - `304`: o servico confirmou que nada mudou. Devolve o corpo guardado.
27
+ * - `200` com `ETag`: corpo novo, que passa a ser o cache.
28
+ * - `200` sem `ETag`: servico antigo ou proxy que removeu o cabecalho. Funciona igual,
29
+ * so nao guarda nada — o proximo boot volta a pedir inteiro.
30
+ */
31
+ export declare function corpoComCache(raiz: string, resposta: Response, cacheAnterior: PayloadCacheado | null): Promise<string | null>;
@@ -0,0 +1,62 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { leConfigDoRepo, leToken } from "./config.js";
5
+ import { escreveAtomico, estadoDoRepo } from "./estado-local.js";
6
+ const caminho = (raiz) => join(estadoDoRepo(raiz), "artefatos.json");
7
+ /**
8
+ * Config e token, hasheados. O mesmo diretorio de estado serve um repositorio, mas o
9
+ * repositorio pode ser reapontado para outro projeto — e ai o ETag guardado e de um
10
+ * acervo que nao e este. Reenvia-lo faria o servico responder `304` e o cliente servir o
11
+ * Brain do projeto anterior, sem nada denunciar. Igual ao cache de ancoras do cinto.
12
+ */
13
+ async function identidade(raiz) {
14
+ const config = await leConfigDoRepo(raiz).catch(() => null);
15
+ return createHash("sha256")
16
+ .update(JSON.stringify([config, config ? await leToken(config.api) : null]))
17
+ .digest("hex");
18
+ }
19
+ /** Sem cache, cache ilegivel, de formato antigo ou de outro projeto: `null` — pede inteiro. */
20
+ export async function leCacheDoPayload(raiz) {
21
+ try {
22
+ const lido = JSON.parse(await readFile(caminho(raiz), "utf8"));
23
+ if (typeof lido.etag !== "string" || typeof lido.corpo !== "string")
24
+ return null;
25
+ if (lido.projeto !== (await identidade(raiz)))
26
+ return null;
27
+ return { etag: lido.etag, corpo: lido.corpo, projeto: lido.projeto };
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ }
33
+ /** Falha em silencio: cache e otimizacao. Nao vale derrubar a sessao por disco cheio. */
34
+ export async function guardaCacheDoPayload(raiz, cache) {
35
+ try {
36
+ await escreveAtomico(caminho(raiz), JSON.stringify({ ...cache, projeto: await identidade(raiz) }));
37
+ }
38
+ catch {
39
+ /* Sem cache o proximo boot so repete a requisicao completa. */
40
+ }
41
+ }
42
+ /**
43
+ * O corpo da resposta, vindo da rede ou do cache — e o cache ja atualizado.
44
+ *
45
+ * Trata as tres situacoes que interessam a quem chama:
46
+ * - `304`: o servico confirmou que nada mudou. Devolve o corpo guardado.
47
+ * - `200` com `ETag`: corpo novo, que passa a ser o cache.
48
+ * - `200` sem `ETag`: servico antigo ou proxy que removeu o cabecalho. Funciona igual,
49
+ * so nao guarda nada — o proximo boot volta a pedir inteiro.
50
+ */
51
+ export async function corpoComCache(raiz, resposta, cacheAnterior) {
52
+ if (resposta.status === 304) {
53
+ // Sem corpo guardado nao ha o que devolver. So acontece se o cache sumir entre a
54
+ // leitura e a resposta; quem chama trata como falha e tenta de novo na proxima.
55
+ return cacheAnterior?.corpo ?? null;
56
+ }
57
+ const corpo = await resposta.text();
58
+ const etag = resposta.headers.get("etag");
59
+ if (etag)
60
+ await guardaCacheDoPayload(raiz, { etag, corpo });
61
+ return corpo;
62
+ }
package/dist/politica.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { cabecalhos, credencial, pede } from "./api.js";
2
+ import { corpoComCache, leCacheDoPayload } from "./payload-cache.js";
2
3
  import { politicaParaSessao } from "./regras-de-commit.js";
3
4
  export async function buscaPolitica(raiz) {
4
5
  let config;
@@ -14,15 +15,31 @@ export async function buscaPolitica(raiz) {
14
15
  url.searchParams.set("tenant", config.config.tenant);
15
16
  url.searchParams.set("projeto", config.config.projeto);
16
17
  try {
17
- const resposta = await pede(url, { headers: cabecalhos(config.token) });
18
- if (!resposta.ok) {
18
+ // O ETag guardado do boot anterior. Com ele, o caso comum — reabrir a sessao sem
19
+ // ninguem ter mexido no Brain — custa um 304 sem corpo, em vez do acervo inteiro.
20
+ const cache = await leCacheDoPayload(raiz);
21
+ const resposta = await pede(url, {
22
+ headers: {
23
+ ...cabecalhos(config.token),
24
+ ...(cache ? { "If-None-Match": cache.etag } : {}),
25
+ },
26
+ });
27
+ // 304 e sucesso, mas `resposta.ok` e falso para ele — checar `ok` primeiro mandaria
28
+ // o caminho feliz do cache direto para "inalcancavel".
29
+ if (!resposta.ok && resposta.status !== 304) {
19
30
  const { erro } = (await resposta.json().catch(() => ({})));
20
31
  return {
21
32
  estado: "inalcancavel",
22
33
  motivo: erro ?? `a API respondeu ${resposta.status}.`,
23
34
  };
24
35
  }
25
- const payload = (await resposta.json());
36
+ const corpo = await corpoComCache(raiz, resposta, cache);
37
+ if (corpo === null) {
38
+ // 304 sem copia local: o cache sumiu entre a leitura e a resposta. Raro, e a sessao
39
+ // seguinte se resolve sozinha pedindo o payload inteiro.
40
+ return { estado: "inalcancavel", motivo: "o cache local do payload sumiu; tente de novo." };
41
+ }
42
+ const payload = JSON.parse(corpo);
26
43
  const conteudo = payload.politica?.trim();
27
44
  const esperandoIndexacao = payload.esperando_indexacao ?? 0;
28
45
  const roadmap = payload.roadmap;
package/dist/reancorar.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { cabecalhos, credencial, pede, recusa } from "./api.js";
2
+ import { invalidaCache } from "./cinto.js";
2
3
  import {} from "./curar.js";
3
4
  /**
4
5
  * Trocar o alvo de uma ancora, sem reescrever a memoria.
@@ -27,9 +28,21 @@ export async function reancora(raiz, endereco, de, para) {
27
28
  const novas = memoria.ancoras.map((a) => (a.valor === de ? { ...a, valor: para } : a));
28
29
  // Reaproveita o PATCH de edicao: ele recebe a memoria inteira, entao mandamos o que ja
29
30
  // estava la com a ancora trocada. Uma porta so para escrever memoria, e nao duas.
31
+ //
32
+ // `If-Match` com o carimbo que acabamos de ler: entre a leitura acima e este PATCH,
33
+ // outra sessao pode ter reescrito a memoria — e como o PATCH e INTEGRAL, gravar assim
34
+ // mesmo apagaria o texto dela por completo para trocar uma ancora. Com a precondicao, o
35
+ // servico recusa com 412 e `recusa` mostra a mensagem que manda reler.
36
+ //
37
+ // Servico mais velho nao devolve `atualizada_em`: sem o campo nao ha cabecalho, e a
38
+ // escrita segue como antes. Melhor que recusar contra um servico que nao sabe do
39
+ // contrato novo.
30
40
  const envio = await pede(`${config.api}/api/v1/memorias/${endereco}`, {
31
41
  method: "PATCH",
32
- headers: cabecalhos(token, true),
42
+ headers: {
43
+ ...cabecalhos(token, true),
44
+ ...(memoria.atualizada_em ? { "If-Match": memoria.atualizada_em } : {}),
45
+ },
33
46
  body: JSON.stringify({
34
47
  tenant: config.tenant,
35
48
  projeto: config.projeto,
@@ -44,5 +57,10 @@ export async function reancora(raiz, endereco, de, para) {
44
57
  });
45
58
  if (!envio.ok)
46
59
  await recusa(envio);
60
+ // O cache de ancoras do cinto e o que o hook consulta para avisar quem toca um arquivo
61
+ // ancorado. Sem invalidar, ele segue avisando no caminho ANTIGO e nao protege o novo —
62
+ // ate alguem reconstruir o cache por outro motivo, o que pode nunca acontecer na mesma
63
+ // sessao em que a refatoracao foi feita.
64
+ await invalidaCache(raiz);
47
65
  return { endereco, de, para };
48
66
  }