dd-harness 0.36.0 → 0.38.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/README.md CHANGED
@@ -1,103 +1,103 @@
1
- # dd-harness
2
-
3
- Política e briefing carregados do serviço; memória consultada por MCP ou CLI.
4
- A fonte dos artefatos continua no serviço. O cliente mantém configuração, ponteiros
5
- para skills e estado descartável fora do repositório.
6
-
7
- Requer Node >=20.3.0. CLI sem dependências de runtime.
8
-
9
- ## Instalação e configuração
10
-
11
- ```sh
12
- npm install -g dd-harness@latest
13
- npx -y dd-harness-mcp@latest --help
14
- dd-harness start --host todos
15
- ```
16
-
17
- Para um projeto já criado no serviço:
18
-
19
- ```sh
20
- dd-harness init --tenant meu-espaco --projeto meu-projeto
21
- dd-harness login --token <token>
22
- dd-harness integrar --host claude,codex,antigravity
23
- dd-harness diagnostico --mcp
24
- ```
25
-
26
- `start` usa Claude por padrão. `--host` aceita `claude`, `codex`,
27
- `antigravity`, uma lista separada por vírgulas ou `todos`.
28
- `integrar` instala nos três por padrão e preserva configurações alheias.
29
- O token nasce em /tokens e fica em ~/.dd-harness/credentials.json, por origem de API.
30
- Para serviço local, use `init --api http://localhost:3000` antes do login.
31
-
32
- ## Hosts
33
-
34
- | Host | MCP | Boot e guarda | Skills |
35
- |---|---|---|---|
36
- | Claude Code | .mcp.json | .claude/settings.json | .claude/skills |
37
- | Codex | .codex/config.toml | .codex/hooks.json | .agents/skills |
38
- | Antigravity | .agents/mcp_config.json | .agents/hooks.json | .agents/skills |
39
-
40
- Codex exige revisão/confiança dos hooks em /hooks. Instalação não comprova ativação.
41
- Antigravity usa PreInvocation e mensagem efêmera: política e briefing são consultados
42
- antes de cada inferência. Sua guarda usa `ask` para respeitar concessões já existentes;
43
- ela pode acrescentar confirmações. Não amplia permissões automaticamente.
44
-
45
- Sem boot válido, a guarda recusa ferramentas cobertas, inclusive shell. Leitura conhecida
46
- e onboarding controlado continuam possíveis. Os hooks precisam estar ativos: não são
47
- sandbox nem controlam ferramentas que o aplicativo não encaminha a eles.
48
- Após boot válido, shell não tem análise de diff antecipada; avisos por âncora cobrem
49
- edições estruturadas, patches, remoções e renomeações.
50
-
51
- Configurações MCP novas levam DD_HARNESS_ROOT explícito. Ao copiar/mover um checkout,
52
- confira essa raiz com diagnostico. O MCP recusa divergência entre a raiz explícita e
53
- o projeto identificado no diretório de execução. Corrija a configuração e reinicie o host.
54
-
55
- ## Uso diário
56
-
57
- ```sh
58
- dd-harness politica
59
- dd-harness buscar "contrato de integração"
60
- dd-harness ler regras/contrato
61
- dd-harness gravar memoria.md
62
- dd-harness editar memoria.md
63
- dd-harness arquivar regras/contrato --motivo obsoleta
64
- dd-harness status
65
- dd-harness skills
66
- dd-harness roadmap
67
- dd-harness changelog
68
- dd-harness --help
69
- ```
70
-
71
- `check` mede e grava observações de deriva; `status` só consulta.
72
- O antigo comando `sync` não faz parte do CLI atual.
73
- `politica --hook` e `cinto` permanecem como compatibilidade legada;
74
- use `integrar` para instalar `sessao` e `guarda`.
75
-
76
- ## Estado e conflitos
77
-
78
- - Política/briefing não são materializados. Âncoras/resumos, sessão e manifestos ficam
79
- em ~/.dd-harness/repos, isolados por caminho/projeto/credencial quando aplicável.
80
- - DD_HARNESS_HOME permite isolamento explícito em testes. Nunca aponte testes ao estado pessoal.
81
- - Uma sessão validada expira em quatro horas. Boot, retomada e compactação revalidam.
82
- Alterações locais de política/briefing via MCP invalidam a sessão; reabra depois de editar.
83
- Alterações remotas feitas por outro cliente são percebidas no próximo boot/revalidação.
84
- - Curadoria invalida as âncoras do checkout atual. A próxima edição as consulta novamente.
85
- - Só ponteiros registrados e intactos são atualizados/removidos. Skills manuais, editadas
86
- ou redirecionadas por links são preservadas e aparecem como conflitos.
87
- - Skills marcadas só por comando não são instaladas para descoberta automática em
88
- .agents; use listar_skills/ler_skill após pedido explícito. Metadados próprios do
89
- Claude não são prometidos como portáveis.
90
- - Se há fila e worker local configurado, o boot solicita um lote. O log fica em
91
- ~/.dd-harness/worker.log; solicitar não significa que a indexação concluiu.
92
-
93
- ## Escritas e commits
94
-
95
- Só GET/HEAD têm retry automático (até três tentativas, timeout por tentativa).
96
- POST/PATCH/PUT/DELETE não são repetidos. Falha de conexão, 5xx ou resposta incompleta
97
- em escrita exige consultar o estado antes de tentar novamente.
98
-
99
- A política entregue exige `[IDENTIFICAÇÃO] - tipo: descrição` nos commits de agentes:
100
- `[CODEX] - feat: ...`, `[CLAUDE] - fix: ...`, `[GEMINI] - docs: ...`.
101
- Identifique quem realmente commitou, inclusive ao usar outro editor. A regra não altera
102
- Git author nem concede autorização de commit/push. É regra de trabalho do agente,
103
- não inferência automática de identidade nem hook que renomeia commits humanos.
1
+ # dd-harness
2
+
3
+ Política e briefing carregados do serviço; memória consultada por MCP ou CLI.
4
+ A fonte dos artefatos continua no serviço. O cliente mantém configuração, ponteiros
5
+ para skills e estado descartável fora do repositório.
6
+
7
+ Requer Node >=20.3.0. CLI sem dependências de runtime.
8
+
9
+ ## Instalação e configuração
10
+
11
+ ```sh
12
+ npm install -g dd-harness@latest
13
+ npx -y dd-harness-mcp@latest --help
14
+ dd-harness start --host todos
15
+ ```
16
+
17
+ Para um projeto já criado no serviço:
18
+
19
+ ```sh
20
+ dd-harness init --tenant meu-espaco --projeto meu-projeto
21
+ dd-harness login --token <token>
22
+ dd-harness integrar --host claude,codex,antigravity
23
+ dd-harness diagnostico --mcp
24
+ ```
25
+
26
+ `start` usa Claude por padrão. `--host` aceita `claude`, `codex`,
27
+ `antigravity`, uma lista separada por vírgulas ou `todos`.
28
+ `integrar` instala nos três por padrão e preserva configurações alheias.
29
+ O token nasce em /tokens e fica em ~/.dd-harness/credentials.json, por origem de API.
30
+ Para serviço local, use `init --api http://localhost:3000` antes do login.
31
+
32
+ ## Hosts
33
+
34
+ | Host | MCP | Boot e guarda | Skills |
35
+ |---|---|---|---|
36
+ | Claude Code | .mcp.json | .claude/settings.json | .claude/skills |
37
+ | Codex | .codex/config.toml | .codex/hooks.json | .agents/skills |
38
+ | Antigravity | .agents/mcp_config.json | .agents/hooks.json | .agents/skills |
39
+
40
+ Codex exige revisão/confiança dos hooks em /hooks. Instalação não comprova ativação.
41
+ Antigravity usa PreInvocation e mensagem efêmera: política e briefing são consultados
42
+ antes de cada inferência. Sua guarda usa `ask` para respeitar concessões já existentes;
43
+ ela pode acrescentar confirmações. Não amplia permissões automaticamente.
44
+
45
+ Sem boot válido, a guarda recusa ferramentas cobertas, inclusive shell. Leitura conhecida
46
+ e onboarding controlado continuam possíveis. Os hooks precisam estar ativos: não são
47
+ sandbox nem controlam ferramentas que o aplicativo não encaminha a eles.
48
+ Após boot válido, shell não tem análise de diff antecipada; avisos por âncora cobrem
49
+ edições estruturadas, patches, remoções e renomeações.
50
+
51
+ Configurações MCP novas levam DD_HARNESS_ROOT explícito. Ao copiar/mover um checkout,
52
+ confira essa raiz com diagnostico. O MCP recusa divergência entre a raiz explícita e
53
+ o projeto identificado no diretório de execução. Corrija a configuração e reinicie o host.
54
+
55
+ ## Uso diário
56
+
57
+ ```sh
58
+ dd-harness politica
59
+ dd-harness buscar "contrato de integração"
60
+ dd-harness ler regras/contrato
61
+ dd-harness gravar memoria.md
62
+ dd-harness editar memoria.md
63
+ dd-harness arquivar regras/contrato --motivo obsoleta
64
+ dd-harness status
65
+ dd-harness skills
66
+ dd-harness roadmap
67
+ dd-harness changelog
68
+ dd-harness --help
69
+ ```
70
+
71
+ `check` mede e grava observações de deriva; `status` só consulta.
72
+ O antigo comando `sync` não faz parte do CLI atual.
73
+ `politica --hook` e `cinto` permanecem como compatibilidade legada;
74
+ use `integrar` para instalar `sessao` e `guarda`.
75
+
76
+ ## Estado e conflitos
77
+
78
+ - Política/briefing não são materializados. Âncoras/resumos, sessão e manifestos ficam
79
+ em ~/.dd-harness/repos, isolados por caminho/projeto/credencial quando aplicável.
80
+ - DD_HARNESS_HOME permite isolamento explícito em testes. Nunca aponte testes ao estado pessoal.
81
+ - Uma sessão validada expira em quatro horas. Boot, retomada e compactação revalidam.
82
+ Alterações locais de política/briefing via MCP invalidam a sessão; reabra depois de editar.
83
+ Alterações remotas feitas por outro cliente são percebidas no próximo boot/revalidação.
84
+ - Curadoria invalida as âncoras do checkout atual. A próxima edição as consulta novamente.
85
+ - Só ponteiros registrados e intactos são atualizados/removidos. Skills manuais, editadas
86
+ ou redirecionadas por links são preservadas e aparecem como conflitos.
87
+ - Skills marcadas só por comando não são instaladas para descoberta automática em
88
+ .agents; use listar_skills/ler_skill após pedido explícito. Metadados próprios do
89
+ Claude não são prometidos como portáveis.
90
+ - Se há fila e worker local configurado, o boot solicita um lote. O log fica em
91
+ ~/.dd-harness/worker.log; solicitar não significa que a indexação concluiu.
92
+
93
+ ## Escritas e commits
94
+
95
+ Só GET/HEAD têm retry automático (até três tentativas, timeout por tentativa).
96
+ POST/PATCH/PUT/DELETE não são repetidos. Falha de conexão, 5xx ou resposta incompleta
97
+ em escrita exige consultar o estado antes de tentar novamente.
98
+
99
+ A política entregue exige `[IDENTIFICAÇÃO] - tipo: descrição` nos commits de agentes:
100
+ `[CODEX] - feat: ...`, `[CLAUDE] - fix: ...`, `[GEMINI] - docs: ...`.
101
+ Identifique quem realmente commitou, inclusive ao usar outro editor. A regra não altera
102
+ Git author nem concede autorização de commit/push. É regra de trabalho do agente,
103
+ não inferência automática de identidade nem hook que renomeia commits humanos.
@@ -21,6 +21,18 @@ export type Item = {
21
21
  * maquina, nao por projeto).
22
22
  */
23
23
  export declare function diagnosticaAtualizacao(raiz: string, versaoInstalada: string, versaoPublicada: string | null): Promise<Item[]>;
24
+ /**
25
+ * Incorpora ao projeto as skills iniciais escolhidas pelo usuario.
26
+ *
27
+ * Recebe os slugs em vez de criar tudo que falta, e essa e a diferenca que importa: skill
28
+ * pode ter sido apagada de proposito, e criar a lista inteira desfaria a decisao de quem
29
+ * apagou. Quem chama aqui e a skill `atualizar-harness`, depois do "sim" a cada uma.
30
+ */
31
+ export declare function incorporaSkills(raiz: string, slugs: string[]): Promise<{
32
+ criadas: string[];
33
+ falharam: string[];
34
+ ponteiros: string;
35
+ }>;
24
36
  /**
25
37
  * Aplica so os itens marcados `aplicar`. Os de `perguntar` ficam intactos de proposito —
26
38
  * quem decide sobre arquivo editado a mao e o dono dele.
package/dist/atualizar.js CHANGED
@@ -74,66 +74,113 @@ export async function diagnosticaAtualizacao(raiz, versaoInstalada, versaoPublic
74
74
  // daquela epoca, e as que vieram depois so existiam em projeto criado do zero. Este item
75
75
  // e o caminho que faltava.
76
76
  //
77
- // Skill inicial que falta e skill que NASCEU DEPOIS deste projeto — as padrao nao se
78
- // apagam, entao ausencia aqui nao e decisao de ninguem, e recria-la nao desfaz nada.
79
- // Por isso `aplicar`: e o mesmo caso do hook que faltava, e nao o do CLAUDE.md editado.
80
- //
81
- // A skill do projeto continua editavel: criar a que falta nao mexe em nenhuma existente,
82
- // e o `criaSkill` erra de proposito se o slug ja existir.
77
+ // `perguntar`, nunca `aplicar`: skill PODE ser apagada, entao do lado de fora "nasceu
78
+ // depois deste projeto" e "eu apaguei porque nao quero" sao indistinguiveis — e recriar
79
+ // sozinho desfaria a segunda em silencio, toda vez que alguem rodasse o comando. Quem
80
+ // sabe qual dos dois e o dono; o diagnostico so mostra o que falta e para que serve.
81
+ let doServico = null;
83
82
  try {
84
- const existentes = new Set((await leSkills(raiz)).map(s => s.slug));
85
- const faltando = SKILLS_INICIAIS.filter(s => !existentes.has(s.slug)).map(s => s.slug);
83
+ doServico = await leSkills(raiz);
84
+ }
85
+ catch {
86
+ // So a REDE cai aqui. O que vem depois e disco, e misturar os dois no mesmo try faz um
87
+ // erro de leitura de arquivo ser reportado como "o serviço não respondeu" — mentira que
88
+ // manda procurar o problema no lugar errado.
89
+ itens.push({ o_que: "skills", situacao: "não consegui consultar o serviço", acao: "manual",
90
+ detalhe: "confira com `dd-harness skills` quando o serviço responder" });
91
+ }
92
+ if (doServico) {
93
+ const existentes = new Set(doServico.map(s => s.slug));
94
+ const faltando = SKILLS_INICIAIS.filter(s => !existentes.has(s.slug));
95
+ // Sem ponteiro em disco, a skill EXISTE no servico e nenhum host a descobre — o Claude
96
+ // Code le `.claude/skills/*/SKILL.md` na abertura da sessao. Some por apagar o .md ou
97
+ // por renomear a pasta, e some CALADA: `dd-harness skills` continua listando a skill,
98
+ // entao a unica pista e ela nunca ser invocada. Caso diferente do de baixo, e este se
99
+ // resolve sozinho: reescrever o ponteiro nao desfaz decisao nenhuma sobre a skill.
100
+ const destinos = [...new Set((await hostsInstalados(raiz))
101
+ .map(h => h === "claude" ? ".claude" : ".agents"))];
102
+ const semPonteiro = [];
103
+ for (const skill of doServico) {
104
+ for (const destino of destinos.length ? destinos : [".claude"]) {
105
+ // No `.agents` a skill so-por-comando nao ganha ponteiro de proposito: ausencia ali
106
+ // e desenho, nao perda.
107
+ if (destino === ".agents" && skill.so_por_comando)
108
+ continue;
109
+ // So ENOENT conta como "sem ponteiro". Permissao negada ou disco com erro nao e
110
+ // ponteiro faltando, e tratar como se fosse mandaria reescrever por cima de um
111
+ // problema que e outro.
112
+ const existe = await readFile(join(raiz, destino, "skills", skill.slug, "SKILL.md"), "utf8")
113
+ .then(() => true)
114
+ .catch((e) => { if (e.code === "ENOENT")
115
+ return false; throw e; });
116
+ if (!existe) {
117
+ semPonteiro.push(`${destino}/skills/${skill.slug}`);
118
+ }
119
+ }
120
+ }
121
+ if (semPonteiro.length) {
122
+ itens.push({
123
+ o_que: "ponteiros de skill",
124
+ situacao: `${semPonteiro.length} skill(s) do serviço sem arquivo em disco`,
125
+ acao: "aplicar",
126
+ detalhe: `${semPonteiro.join(", ")} — o host não descobre a skill sem o ponteiro; será reescrito`,
127
+ });
128
+ }
86
129
  itens.push(faltando.length === 0
87
130
  ? { o_que: "skills", situacao: `${existentes.size} no projeto; nenhuma inicial faltando`, acao: "em-dia" }
88
131
  : {
89
132
  o_que: "skills",
90
- situacao: `${faltando.length} skill(s) inicial(is) nasceram depois deste projeto`,
91
- acao: "aplicar",
92
- detalhe: `${faltando.join(", ")} — serão criadas no serviço; as existentes não são tocadas`,
133
+ situacao: `${faltando.length} skill(s) inicial(is) não estão neste projeto`,
134
+ acao: "perguntar",
135
+ // A descricao inteira, nao so o slug: e o campo que diz QUANDO a skill serve, e
136
+ // sem ele a pergunta vira "quer `revisar-memoria`?" — que ninguem responde bem.
137
+ detalhe: faltando.map(s => `${s.slug}: ${s.descricao}`).join("\n"),
93
138
  });
94
139
  }
95
- catch {
96
- // Sem rede ou sem credencial: o resto do diagnostico continua valendo, e skill faltando
97
- // nao e urgente o bastante para derrubar a checagem inteira.
98
- itens.push({ o_que: "skills", situacao: "não consegui consultar o serviço", acao: "manual",
99
- detalhe: "confira com `dd-harness skills` quando o serviço responder" });
100
- }
101
140
  return itens;
102
141
  }
103
- /** Cria no serviço as skills iniciais que este projeto ainda não tem. */
104
- async function criaSkillsFaltantes(raiz) {
105
- const existentes = new Set((await leSkills(raiz)).map(s => s.slug));
106
- const faltando = SKILLS_INICIAIS.filter(s => !existentes.has(s.slug));
142
+ /** Reescreve em disco os ponteiros das skills que o serviço tem. */
143
+ async function reescrevePonteiros(raiz) {
144
+ const destinos = [...new Set((await hostsInstalados(raiz))
145
+ .map(h => h === "claude" ? ".claude" : ".agents"))];
146
+ const r = await escrevePonteirosDeSkills(raiz, await leSkills(raiz), destinos.length ? destinos : [".claude"]);
147
+ return `ponteiros de skill: ${r.escritos} reescrito(s)` +
148
+ (r.conflitos.length ? `; ${r.conflitos.length} preservado(s) por edição local` : "");
149
+ }
150
+ /**
151
+ * Incorpora ao projeto as skills iniciais escolhidas pelo usuario.
152
+ *
153
+ * Recebe os slugs em vez de criar tudo que falta, e essa e a diferenca que importa: skill
154
+ * pode ter sido apagada de proposito, e criar a lista inteira desfaria a decisao de quem
155
+ * apagou. Quem chama aqui e a skill `atualizar-harness`, depois do "sim" a cada uma.
156
+ */
157
+ export async function incorporaSkills(raiz, slugs) {
107
158
  const criadas = [];
108
159
  const falharam = [];
109
- for (const inicial of faltando) {
160
+ for (const slug of slugs) {
161
+ const inicial = SKILLS_INICIAIS.find(s => s.slug === slug);
162
+ if (!inicial) {
163
+ falharam.push(`${slug} (não é uma skill inicial)`);
164
+ continue;
165
+ }
110
166
  try {
111
167
  await criaSkill(raiz, inicial);
112
- criadas.push(inicial.slug);
168
+ criadas.push(slug);
113
169
  }
114
- catch {
115
- // Uma que nao entrou nao impede as outras: metade criada e melhor que nenhuma, e a
116
- // linha de retorno diz exatamente qual faltou.
117
- falharam.push(inicial.slug);
170
+ catch (erro) {
171
+ // Uma que nao entrou nao impede as outras: metade incorporada e melhor que nenhuma,
172
+ // e o retorno diz qual faltou e por que.
173
+ falharam.push(`${slug} (${erro instanceof Error ? erro.message : String(erro)})`);
118
174
  }
119
175
  }
176
+ // Sem o ponteiro em disco a skill existe no servico e host nenhum a descobre; criar e
177
+ // parar aqui entregaria uma skill invisivel ate o boot seguinte.
120
178
  let ponteiros = "";
121
- // A skill no servico nao e descoberta por host nenhum sem o ponteiro em disco: o Claude
122
- // Code le `.claude/skills/*/SKILL.md` na abertura da sessao, e MCP nao fornece skill.
123
- // Criar no servico e parar aqui entregaria uma skill que so aparece no boot seguinte.
124
179
  if (criadas.length) {
125
- try {
126
- const hosts = await hostsInstalados(raiz);
127
- const destinos = [...new Set(hosts.map(h => h === "claude" ? ".claude" : ".agents"))];
128
- const r = await escrevePonteirosDeSkills(raiz, await leSkills(raiz), destinos.length ? destinos : [".claude"]);
129
- ponteiros = `; ${r.escritos} ponteiro(s) em disco`;
130
- }
131
- catch {
132
- ponteiros = "; ponteiros NÃO escritos — rode `dd-harness integrar` ou reabra a sessão";
133
- }
180
+ ponteiros = await reescrevePonteiros(raiz)
181
+ .catch(() => "ponteiros NÃO escritos — rode `dd-harness atualizar --aplicar` ou reabra a sessão");
134
182
  }
135
- return `skills: ${criadas.length} criada(s)${criadas.length ? " — " + criadas.join(", ") : ""}` +
136
- (falharam.length ? `; FALHARAM: ${falharam.join(", ")}` : "") + ponteiros;
183
+ return { criadas, falharam, ponteiros };
137
184
  }
138
185
  /**
139
186
  * Aplica so os itens marcados `aplicar`. Os de `perguntar` ficam intactos de proposito —
@@ -152,12 +199,14 @@ export async function aplicaAtualizacao(raiz, itens) {
152
199
  // um projeto que continua sem o cinto.
153
200
  feitos.push(r.ok ? `hooks: ${r.estado}` : `hooks: NÃO aplicado (${r.motivo})`);
154
201
  }
155
- else if (item.o_que === "skills") {
202
+ else if (item.o_que === "ponteiros de skill") {
203
+ // Seguro no `--aplicar`: reescrever o ponteiro de uma skill que o servico tem nao
204
+ // decide nada sobre a skill — so devolve ao host a chance de descobri-la.
156
205
  try {
157
- feitos.push(await criaSkillsFaltantes(raiz));
206
+ feitos.push(await reescrevePonteiros(raiz));
158
207
  }
159
208
  catch (erro) {
160
- feitos.push(`skills: NÃO aplicado (${erro instanceof Error ? erro.message : String(erro)})`);
209
+ feitos.push(`ponteiros de skill: NÃO aplicado (${erro instanceof Error ? erro.message : String(erro)})`);
161
210
  }
162
211
  }
163
212
  }
@@ -5,9 +5,14 @@ import { achaRaiz, leConfigDoRepo, leToken } from "./config.js";
5
5
  import { buscaPolitica } from "./politica.js";
6
6
  export async function handshakeMcp(raiz, comando = "npx", args = ["-y", "dd-harness-mcp@latest"]) {
7
7
  return new Promise((resolve, reject) => {
8
+ // `DD_HARNESS_ROOT` fica FORA: passa-la aqui testaria um cenario que o host nao
9
+ // reproduz, e esconderia justamente a falha procurada — com a raiz explicita correta o
10
+ // servidor sobe ate quando a declaracao versionada esta quebrada para outra maquina.
11
+ // O cwd e a raiz, que e como o host lanca.
12
+ const { DD_HARNESS_ROOT: _ignorado, ...ambiente } = process.env;
8
13
  const filho = spawn(comando, args, { cwd: raiz, windowsHide: true,
9
14
  shell: process.platform === "win32" && comando === "npx", stdio: ["pipe", "pipe", "pipe"],
10
- env: { ...process.env, DD_HARNESS_ROOT: raiz } });
15
+ env: ambiente });
11
16
  let buffer = "", terminou = false, versao = "desconhecida";
12
17
  const fim = (erro, resultado) => {
13
18
  if (terminou)
@@ -79,8 +84,13 @@ export async function diagnostico(inicio, testarMcp = false) {
79
84
  const h = await readFile(join(raiz, hook), "utf8").catch(() => "");
80
85
  linhas.push(`${host}: MCP ${m.includes("dd-harness") ? "declarado" : "ausente"}; ` +
81
86
  `hooks ${h.includes("dd-harness guarda") && h.includes("dd-harness sessao") ? "declarados" : "ausentes/legados"}; ativação no host requer verificação.`);
82
- if (m.includes("dd-harness") && !m.includes(JSON.stringify(raiz)))
83
- linhas.push(`${host}: confira DD_HARNESS_ROOT; a raiz explícita atual não foi reconhecida.`);
87
+ // Invertido de proposito: a raiz explicita era exigida, e passou a ser o defeito. O
88
+ // arquivo vai para o git, entao um caminho absoluto so vale na maquina que gerou —
89
+ // noutro checkout o servidor lanca antes do handshake e o host so ve conexao fechada.
90
+ if (m.includes("dd-harness") && m.includes("DD_HARNESS_ROOT")) {
91
+ linhas.push(`${host}: DD_HARNESS_ROOT declarado em ${config} — remova. ` +
92
+ `Caminho absoluto em arquivo versionado quebra em outro checkout; sem ele a raiz sai do cwd.`);
93
+ }
84
94
  }
85
95
  const politica = await buscaPolitica(raiz);
86
96
  linhas.push(`Contexto: ${politica.estado}${politica.estado === "inalcancavel" ? " — " + politica.motivo : ""}`);
@@ -140,28 +140,28 @@ export const escreveAgents = (raiz) => escrevePonteiro(raiz, "AGENTS.md");
140
140
  * em producao compara para saber se precisa de atualizacao.
141
141
  */
142
142
  export const VERSAO_DO_MOLDE = "v1.3.0";
143
- export const SUGESTAO_AGENTS = `## Protocolo do dd-harness
144
-
145
- Este projeto tem um protocolo de trabalho que **não está neste arquivo**.
146
-
147
- **ANTES de qualquer outra coisa**, chame a ferramenta MCP \`ler_artefato\` com
148
- \`tipo: "politica"\` e siga o que ela devolver. É a primeira ação da sessão, antes
149
- de ler código, responder ou planejar.
150
-
151
- - Se a ferramenta não existir, o servidor MCP do dd-harness não está declarado:
152
- avise o usuário e **não modifique nada** até ele resolver.
153
- - Se ela devolver vazio, este projeto nunca foi briefado — diga isso ao usuário.
154
-
155
- Leia também o briefing com a mesma ferramenta (tipo briefing). Os dois são obrigatórios.
156
-
157
- Depois, \`ler_roadmap\`: se houver uma fase **Agora**, é dela que saem os passos
158
- desta sessão. Lista vazia significa que este projeto não usa roadmap, e isso é
159
- válido — não crie fase sem o usuário pedir.
160
-
161
- E \`listar_skills\`: são os procedimentos deste projeto. **Invoque a que couber
162
- ANTES de fazer o trabalho, não depois** — skill lida no fim vira revisão do que
163
- já saiu errado, e é tarde.
164
-
165
- Vale mesmo quando o pedido parece pequeno: "é só um ajuste" é exatamente a
166
- frase que antecede pular o procedimento. A política diz quais são obrigatórias
143
+ export const SUGESTAO_AGENTS = `## Protocolo do dd-harness
144
+
145
+ Este projeto tem um protocolo de trabalho que **não está neste arquivo**.
146
+
147
+ **ANTES de qualquer outra coisa**, chame a ferramenta MCP \`ler_artefato\` com
148
+ \`tipo: "politica"\` e siga o que ela devolver. É a primeira ação da sessão, antes
149
+ de ler código, responder ou planejar.
150
+
151
+ - Se a ferramenta não existir, o servidor MCP do dd-harness não está declarado:
152
+ avise o usuário e **não modifique nada** até ele resolver.
153
+ - Se ela devolver vazio, este projeto nunca foi briefado — diga isso ao usuário.
154
+
155
+ Leia também o briefing com a mesma ferramenta (tipo briefing). Os dois são obrigatórios.
156
+
157
+ Depois, \`ler_roadmap\`: se houver uma fase **Agora**, é dela que saem os passos
158
+ desta sessão. Lista vazia significa que este projeto não usa roadmap, e isso é
159
+ válido — não crie fase sem o usuário pedir.
160
+
161
+ E \`listar_skills\`: são os procedimentos deste projeto. **Invoque a que couber
162
+ ANTES de fazer o trabalho, não depois** — skill lida no fim vira revisão do que
163
+ já saiu errado, e é tarde.
164
+
165
+ Vale mesmo quando o pedido parece pequeno: "é só um ajuste" é exatamente a
166
+ frase que antecede pular o procedimento. A política diz quais são obrigatórias
167
167
  e quando.`;
package/dist/hosts.js CHANGED
@@ -1,4 +1,5 @@
1
- import { readFile, mkdir } from "node:fs/promises";
1
+ import { execFile } from "node:child_process";
2
+ import { access, readFile, mkdir } from "node:fs/promises";
2
3
  import { dirname, join, resolve } from "node:path";
3
4
  import { escreveAtomico, estadoDoRepo } from "./estado-local.js";
4
5
  import { escrevePonteiro, SUGESTAO_AGENTS } from "./escreve-config.js";
@@ -48,11 +49,84 @@ function grupo(doc, evento, command, antigos = []) {
48
49
  preservados.push({ hooks: [{ type: "command", command, timeout: 60 }] });
49
50
  doc[evento] = preservados;
50
51
  }
52
+ /**
53
+ * Onde o `dd-harness-mcp` instalado guarda o arquivo de entrada, ou `null` se nao houver.
54
+ *
55
+ * O caminho sai do `bin` do proprio pacote, nao de um literal daqui: ele ja mudou uma vez
56
+ * (`dist/mcp/src/`, porque o build abrange dois diretorios) e um literal duplicado
57
+ * envelheceria em silencio — apontando para um arquivo que nao existe, o que o host
58
+ * reporta como CONNECTION_CLOSED sem dizer mais nada.
59
+ */
60
+ async function entradaDoMcp() {
61
+ // `devolve` e nao `resolve`: o `resolve` de `node:path` esta no escopo deste modulo, e
62
+ // o parametro o sombrearia dentro do callback.
63
+ const raizGlobal = await new Promise(devolve => {
64
+ execFile("npm", ["root", "-g"], { shell: process.platform === "win32" }, (erro, saida) => devolve(erro ? "" : saida.trim()));
65
+ });
66
+ if (!raizGlobal)
67
+ return null;
68
+ const pasta = join(raizGlobal, "dd-harness-mcp");
69
+ try {
70
+ const { bin } = JSON.parse(await readFile(join(pasta, "package.json"), "utf8"));
71
+ const relativo = typeof bin === "string" ? bin : bin?.["dd-harness-mcp"];
72
+ if (!relativo)
73
+ return null;
74
+ const entrada = join(pasta, relativo);
75
+ await access(entrada);
76
+ return entrada;
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ /**
83
+ * Config de MCP e artefato de MAQUINA, nao de projeto: o bloco carrega o caminho do node
84
+ * e do pacote instalado ali. Versionar isso entrega a um clone o valor de quem gerou, e o
85
+ * sintoma no outro lado e sempre o mesmo CONNECTION_CLOSED — que nao diz nada sobre a
86
+ * causa. Quem clona roda `integrar` e recebe o seu.
87
+ *
88
+ * Acrescenta ao que ja existir, sem reescrever: `.gitignore` e arquivo do projeto.
89
+ */
90
+ async function ignoraConfigDeMaquina(raiz, caminhos) {
91
+ const arquivo = join(raiz, ".gitignore");
92
+ const atual = await readFile(arquivo, "utf8").catch((e) => {
93
+ if (e.code === "ENOENT")
94
+ return "";
95
+ throw e;
96
+ });
97
+ const linhas = new Set(atual.split(/\r?\n/).map(l => l.trim()));
98
+ const faltam = caminhos.filter(c => !linhas.has(c));
99
+ if (!faltam.length)
100
+ return;
101
+ const bloco = "# dd-harness: config de MCP e por maquina — rode `dd-harness integrar`\n" + faltam.join("\n") + "\n";
102
+ await escreveAtomico(arquivo, atual && !atual.endsWith("\n") ? `${atual}\n${bloco}` : atual + bloco);
103
+ }
51
104
  /** Cada host usa seu formato. Configuração alheia não é inferida como gerenciada. */
52
105
  export async function instalaHosts(raiz, hosts) {
53
106
  raiz = resolve(raiz);
54
107
  const avisos = [];
55
- const mcp = { command: "npx", args: ["-y", "dd-harness-mcp@latest"], env: { DD_HARNESS_ROOT: raiz } };
108
+ // `npx` NAO serve no Windows, e nao ha variante que sirva: o host lanca o servidor com
109
+ // `spawn` sem shell, e ali `npx` da ENOENT (nao existe executavel com esse nome) e
110
+ // `npx.cmd` da EINVAL (o Node recusa .bat/.cmd sem shell desde a CVE-2024-27980). Os
111
+ // dois chegam ao cliente como CONNECTION_CLOSED, que parece servidor morrendo e nao
112
+ // comando inalcancavel. Apontar o proprio node para o arquivo e o que sobra — e e o que
113
+ // todo MCP que funciona nesta plataforma ja faz.
114
+ //
115
+ // Gravar caminho de maquina aqui so e legitimo porque estes arquivos sao de maquina: o
116
+ // `integrar` os poe no .gitignore, e quem clona roda `integrar` e recebe o seu. Num
117
+ // arquivo versionado seria o mesmo erro que DD_HARNESS_ROOT ja foi.
118
+ //
119
+ // Sem DD_HARNESS_ROOT: `raizDoMcp` resolve a raiz pelo cwd, que e onde o host lanca o
120
+ // servidor. Com a variavel, um caminho de outro checkout o faz lancar antes do
121
+ // handshake — mesmo CONNECTION_CLOSED, outra causa.
122
+ const entrada = await entradaDoMcp();
123
+ const mcp = entrada
124
+ ? { command: process.execPath, args: [entrada] }
125
+ : { command: "npx", args: ["-y", "dd-harness-mcp@latest"] };
126
+ if (!entrada && process.platform === "win32") {
127
+ avisos.push("MCP declarado via npx, que nao sobe no Windows: instale com " +
128
+ "`npm i -g dd-harness-mcp@latest` e rode `dd-harness integrar` de novo.");
129
+ }
56
130
  for (const host of hosts) {
57
131
  if (host === "codex") {
58
132
  const caminho = join(raiz, ".codex", "config.toml");
@@ -62,10 +136,11 @@ export async function instalaHosts(raiz, hosts) {
62
136
  throw e;
63
137
  });
64
138
  if (atual.includes("dd-harness")) {
65
- avisos.push("Codex: servidor já configurado; preservado. Confira command e DD_HARNESS_ROOT com diagnostico.");
139
+ avisos.push("Codex: servidor já configurado; preservado. Confira o command com diagnostico.");
66
140
  }
67
141
  else {
68
- await escreveAtomico(caminho, atual + `\n[mcp_servers.dd-harness]\ncommand = "npx"\nargs = ["-y", "dd-harness-mcp@latest"]\nstartup_timeout_sec = 60\n[mcp_servers.dd-harness.env]\nDD_HARNESS_ROOT = ${JSON.stringify(raiz)}\n`);
142
+ const args = mcp.args.map(a => JSON.stringify(a)).join(", ");
143
+ await escreveAtomico(caminho, atual + `\n[mcp_servers.dd-harness]\ncommand = ${JSON.stringify(mcp.command)}\nargs = [${args}]\nstartup_timeout_sec = 60\n`);
69
144
  }
70
145
  await json(join(raiz, ".codex", "hooks.json"), doc => {
71
146
  const h = objeto(doc.hooks);
@@ -120,6 +195,7 @@ export async function instalaHosts(raiz, hosts) {
120
195
  }
121
196
  }
122
197
  }
198
+ await ignoraConfigDeMaquina(raiz, hosts.map(h => h === "claude" ? ".mcp.json" : h === "codex" ? ".codex/config.toml" : ".agents/mcp_config.json"));
123
199
  await mkdir(dirname(join(raiz, "AGENTS.md")), { recursive: true });
124
200
  await escrevePonteiro(raiz, "AGENTS.md");
125
201
  if (hosts.includes("claude"))