dd-harness 0.30.0 → 0.32.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.
@@ -0,0 +1,52 @@
1
+ import { resolve } from "node:path";
2
+ const texto = (v) => typeof v === "string" ? v : undefined;
3
+ export function normalizaEntrada(host, entrada) {
4
+ if (host === "antigravity") {
5
+ const roots = entrada.workspacePaths;
6
+ if (!Array.isArray(roots) || roots.length !== 1 || typeof roots[0] !== "string") {
7
+ throw new Error("dd-harness exige um workspace inequívoco por sessão; selecione um projeto.");
8
+ }
9
+ const chamada = entrada.toolCall;
10
+ return { raiz: roots[0], sessao: texto(entrada.conversationId) ?? "", ferramenta: chamada?.name ?? "",
11
+ args: chamada?.args ?? {} };
12
+ }
13
+ return { raiz: texto(entrada.cwd) ?? process.cwd(), sessao: texto(entrada.session_id) ?? "",
14
+ ferramenta: texto(entrada.tool_name) ?? "", args: (entrada.tool_input ?? {}) };
15
+ }
16
+ export function edicoesDaChamada(c) {
17
+ const a = c.args;
18
+ if (c.ferramenta === "apply_patch") {
19
+ const patch = texto(a.command) ?? texto(a.patch) ?? "";
20
+ const edicoes = [];
21
+ let atual;
22
+ for (const linha of patch.split(/\r?\n/)) {
23
+ const inicio = /^\*\*\* (Update|Add|Delete) File: (.+)$/.exec(linha);
24
+ if (inicio) {
25
+ atual = { arquivo: resolve(c.raiz, inicio[2]), antes: inicio[1] === "Delete" ? undefined : "", depois: "" };
26
+ edicoes.push(atual);
27
+ }
28
+ else if (atual && linha.startsWith("*** Move to: ")) {
29
+ // Renomear afeta o arquivo inteiro, inclusive trechos fora dos hunks.
30
+ atual.antes = undefined;
31
+ edicoes.push({ arquivo: resolve(c.raiz, linha.slice(13)), origem: atual.arquivo });
32
+ }
33
+ else if (atual && !linha.startsWith("***")) {
34
+ if (atual.antes !== undefined && (linha.startsWith("-") || linha.startsWith(" ")))
35
+ atual.antes += linha.slice(1) + "\n";
36
+ if (linha.startsWith("+") || linha.startsWith(" "))
37
+ atual.depois += linha.slice(1) + "\n";
38
+ }
39
+ }
40
+ return edicoes;
41
+ }
42
+ const arquivo = texto(a.file_path) ?? texto(a.TargetFile) ?? texto(a.Path);
43
+ if (!arquivo)
44
+ return [];
45
+ const chunks = a.ReplacementChunks ?? a.edits;
46
+ if (Array.isArray(chunks))
47
+ return chunks.map(chunk => ({ arquivo: resolve(c.raiz, arquivo),
48
+ antes: texto(chunk.TargetContent ?? chunk.old_string), depois: texto(chunk.ReplacementContent ?? chunk.new_string) }));
49
+ return [{ arquivo: resolve(c.raiz, arquivo),
50
+ antes: texto(a.old_string) ?? texto(a.TargetContent),
51
+ depois: texto(a.new_string) ?? texto(a.content) ?? texto(a.ReplacementContent) ?? texto(a.CodeContent) }];
52
+ }
@@ -50,3 +50,4 @@ export declare function escrevePonteiro(raiz: string, arquivo: "AGENTS.md" | "CL
50
50
  export declare const escreveAgents: (raiz: string) => Promise<{
51
51
  estado: "criado" | "ja-tinha" | "acrescentado";
52
52
  }>;
53
+ export declare const SUGESTAO_AGENTS = "## Protocolo do dd-harness\n\nEste projeto tem um protocolo de trabalho que **n\u00E3o est\u00E1 neste arquivo**.\n\n**ANTES de qualquer outra coisa**, chame a ferramenta MCP `ler_artefato` com\n`tipo: \"politica\"` e siga o que ela devolver. \u00C9 a primeira a\u00E7\u00E3o da sess\u00E3o, antes\nde ler c\u00F3digo, responder ou planejar.\n\n- Se a ferramenta n\u00E3o existir, o servidor MCP do dd-harness n\u00E3o est\u00E1 declarado:\n avise o usu\u00E1rio e **n\u00E3o modifique nada** at\u00E9 ele resolver.\n- Se ela devolver vazio, este projeto nunca foi briefado \u2014 diga isso ao usu\u00E1rio.\n\nLeia tamb\u00E9m o briefing com a mesma ferramenta (tipo briefing). Os dois s\u00E3o obrigat\u00F3rios.\n\nDepois, `ler_roadmap`: se houver uma fase **Agora**, \u00E9 dela que saem os passos\ndesta sess\u00E3o. Lista vazia significa que este projeto n\u00E3o usa roadmap, e isso \u00E9\nv\u00E1lido \u2014 n\u00E3o crie fase sem o usu\u00E1rio pedir.\n\nE `listar_skills`: s\u00E3o os procedimentos deste projeto. **Invoque a que couber\nANTES de fazer o trabalho, n\u00E3o depois** \u2014 skill lida no fim vira revis\u00E3o do que\nj\u00E1 saiu errado, e \u00E9 tarde.\n\nVale mesmo quando o pedido parece pequeno: \"\u00E9 s\u00F3 um ajuste\" \u00E9 exatamente a\nfrase que antecede pular o procedimento. A pol\u00EDtica diz quais s\u00E3o obrigat\u00F3rias\ne quando.";
@@ -112,7 +112,7 @@ export async function escrevePonteiro(raiz, arquivo) {
112
112
  }
113
113
  /** Compatibilidade: `init` ainda chama pelo nome antigo. */
114
114
  export const escreveAgents = (raiz) => escrevePonteiro(raiz, "AGENTS.md");
115
- const SUGESTAO_AGENTS = `## Protocolo do dd-harness
115
+ export const SUGESTAO_AGENTS = `## Protocolo do dd-harness
116
116
 
117
117
  Este projeto tem um protocolo de trabalho que **não está neste arquivo**.
118
118
 
@@ -124,10 +124,16 @@ de ler código, responder ou planejar.
124
124
  avise o usuário e **não modifique nada** até ele resolver.
125
125
  - Se ela devolver vazio, este projeto nunca foi briefado — diga isso ao usuário.
126
126
 
127
- Depois, \`ler_roadmap\`: se houver uma fase **Agora**, é dela que saem os passos
127
+ Leia também o briefing com a mesma ferramenta (tipo briefing). Os dois são obrigatórios.
128
+
129
+ Depois, \`ler_roadmap\`: se houver uma fase **Agora**, é dela que saem os passos
128
130
  desta sessão. Lista vazia significa que este projeto não usa roadmap, e isso é
129
131
  válido — não crie fase sem o usuário pedir.
130
132
 
131
133
  E \`listar_skills\`: são os procedimentos deste projeto. **Invoque a que couber
132
134
  ANTES de fazer o trabalho, não depois** — skill lida no fim vira revisão do que
133
- já saiu errado. A política diz quais são obrigatórias e quando.`;
135
+ já saiu errado, e é tarde.
136
+
137
+ Vale mesmo quando o pedido parece pequeno: "é só um ajuste" é exatamente a
138
+ frase que antecede pular o procedimento. A política diz quais são obrigatórias
139
+ e quando.`;
@@ -0,0 +1,7 @@
1
+ /** Override explícito para testes e instalações isoladas; nunca troca HOME do processo. */
2
+ export declare const diretorioDoUsuario: () => string;
3
+ export declare function estadoDoRepo(raiz: string): string;
4
+ export declare function revisaoDoContexto(raiz: string): Promise<string>;
5
+ export declare function invalidaContexto(raiz: string): Promise<void>;
6
+ /** Leitores veem o JSON anterior ou o novo, nunca um arquivo parcialmente escrito. */
7
+ export declare function escreveAtomico(caminho: string, conteudo: string): Promise<void>;
@@ -0,0 +1,33 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { mkdir, rename, writeFile, rm, readFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join, resolve } from "node:path";
5
+ /** Override explícito para testes e instalações isoladas; nunca troca HOME do processo. */
6
+ export const diretorioDoUsuario = () => process.env.DD_HARNESS_HOME ?? join(homedir(), ".dd-harness");
7
+ export function estadoDoRepo(raiz) {
8
+ const caminho = resolve(raiz);
9
+ const chave = createHash("sha256").update(process.platform === "win32" ? caminho.toLowerCase() : caminho).digest("hex");
10
+ return join(diretorioDoUsuario(), "repos", chave);
11
+ }
12
+ export async function revisaoDoContexto(raiz) {
13
+ return readFile(join(estadoDoRepo(raiz), "contexto-revisao"), "utf8").catch((e) => {
14
+ if (e.code === "ENOENT")
15
+ return "";
16
+ throw e;
17
+ });
18
+ }
19
+ export async function invalidaContexto(raiz) {
20
+ await escreveAtomico(join(estadoDoRepo(raiz), "contexto-revisao"), randomUUID());
21
+ }
22
+ /** Leitores veem o JSON anterior ou o novo, nunca um arquivo parcialmente escrito. */
23
+ export async function escreveAtomico(caminho, conteudo) {
24
+ await mkdir(dirname(caminho), { recursive: true });
25
+ const temporario = `${caminho}.${randomUUID()}.tmp`;
26
+ try {
27
+ await writeFile(temporario, conteudo, { encoding: "utf8", mode: 0o600 });
28
+ await rename(temporario, caminho);
29
+ }
30
+ finally {
31
+ await rm(temporario, { force: true });
32
+ }
33
+ }
package/dist/gravar.js CHANGED
@@ -1,7 +1,7 @@
1
+ import { invalidaCache } from "./cinto.js";
1
2
  import { readFile } from "node:fs/promises";
2
3
  import { recusaPorAcentuacao } from "./acentuacao.js";
3
- import { cabecalhos, credencial, pede, recusa } from "./api.js";
4
- import { acrescentaAoCache } from "./cinto.js";
4
+ import { jsonDaEscrita, cabecalhos, credencial, pede, recusa } from "./api.js";
5
5
  const OBRIGATORIOS = ["name", "titulo", "description", "pasta"];
6
6
  /**
7
7
  * Frontmatter simples: `chave: valor` por linha. Sem lib de YAML — nao ha aninhamento.
@@ -188,19 +188,9 @@ export async function grava(raiz, caminho) {
188
188
  });
189
189
  if (!resposta.ok)
190
190
  await recusa(resposta);
191
- const { endereco } = (await resposta.json());
192
- // O cinto passa a enxergar esta memoria AGORA, nao na proxima sessao. Sem isto, a
193
- // memoria recem-gravada nao protegia nada ate a sessao seguinte medido na rodada 006,
194
- // e justamente no fluxo mais comum: gravar a decisao e, minutos depois, alguem tentar
195
- // desfazer o que ela guarda.
196
- await acrescentaAoCache(raiz, {
197
- pasta: memoria.pasta,
198
- slug: memoria.slug,
199
- titulo: memoria.titulo,
200
- resumo: memoria.resumo,
201
- status: "ativa",
202
- ancoras: memoria.ancoras.map((valor) => ({ tipo: "caminho", valor, sha: null })),
203
- });
191
+ await invalidaCache(raiz);
192
+ const { endereco } = (await jsonDaEscrita(resposta));
193
+ // A próxima edição atualiza o cache inteiro, incluindo outras memórias e globais.
204
194
  return {
205
195
  endereco,
206
196
  ancoras: memoria.ancoras.length,
@@ -0,0 +1,6 @@
1
+ import type { Host } from "./edicoes.js";
2
+ export declare const HOSTS: Host[];
3
+ export declare function selecionaHosts(valor?: string): Host[];
4
+ export declare function hostsInstalados(raiz: string): Promise<Host[]>;
5
+ /** Cada host usa seu formato. Configuração alheia não é inferida como gerenciada. */
6
+ export declare function instalaHosts(raiz: string, hosts: Host[]): Promise<string[]>;
package/dist/hosts.js ADDED
@@ -0,0 +1,129 @@
1
+ import { readFile, mkdir } from "node:fs/promises";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { escreveAtomico, estadoDoRepo } from "./estado-local.js";
4
+ import { escrevePonteiro, SUGESTAO_AGENTS } from "./escreve-config.js";
5
+ export const HOSTS = ["claude", "codex", "antigravity"];
6
+ export function selecionaHosts(valor = "claude") {
7
+ const nomes = valor === "todos" ? HOSTS : valor.split(",");
8
+ if (nomes.some(n => !HOSTS.includes(n)))
9
+ throw new Error("host: use claude, codex, antigravity ou todos");
10
+ return [...new Set(nomes)];
11
+ }
12
+ export async function hostsInstalados(raiz) {
13
+ try {
14
+ return selecionaHosts(JSON.parse(await readFile(join(estadoDoRepo(raiz), "hosts.json"), "utf8")).join(","));
15
+ }
16
+ catch {
17
+ return [];
18
+ }
19
+ }
20
+ function objeto(v) {
21
+ if (v === undefined)
22
+ return {};
23
+ if (!v || typeof v !== "object" || Array.isArray(v))
24
+ throw new Error("estrutura de configuração inválida");
25
+ return v;
26
+ }
27
+ async function json(caminho, altera) {
28
+ const atual = await readFile(caminho, "utf8").catch((e) => {
29
+ if (e.code === "ENOENT")
30
+ return "{}";
31
+ throw e;
32
+ });
33
+ const doc = objeto(JSON.parse(atual));
34
+ altera(doc);
35
+ const novo = JSON.stringify(doc, null, 2) + "\n";
36
+ if (atual !== novo)
37
+ await escreveAtomico(caminho, novo);
38
+ }
39
+ function grupo(doc, evento, command, antigos = []) {
40
+ const grupos = doc[evento] ?? [];
41
+ if (!Array.isArray(grupos))
42
+ throw new Error(`hooks.${evento} precisa ser lista`);
43
+ const preservados = grupos.map(g => {
44
+ if (!g || typeof g !== "object" || !Array.isArray(g.hooks))
45
+ throw new Error(`hook ${evento} inválido`);
46
+ return { ...g, hooks: g.hooks.filter((h) => ![command, ...antigos].includes(h.command ?? "")) };
47
+ }).filter(g => g.hooks.length);
48
+ preservados.push({ hooks: [{ type: "command", command, timeout: 60 }] });
49
+ doc[evento] = preservados;
50
+ }
51
+ /** Cada host usa seu formato. Configuração alheia não é inferida como gerenciada. */
52
+ export async function instalaHosts(raiz, hosts) {
53
+ raiz = resolve(raiz);
54
+ const avisos = [];
55
+ const mcp = { command: "npx", args: ["-y", "dd-harness-mcp@latest"], env: { DD_HARNESS_ROOT: raiz } };
56
+ for (const host of hosts) {
57
+ if (host === "codex") {
58
+ const caminho = join(raiz, ".codex", "config.toml");
59
+ const atual = await readFile(caminho, "utf8").catch((e) => {
60
+ if (e.code === "ENOENT")
61
+ return "";
62
+ throw e;
63
+ });
64
+ if (atual.includes("dd-harness")) {
65
+ avisos.push("Codex: servidor já configurado; preservado. Confira command e DD_HARNESS_ROOT com diagnostico.");
66
+ }
67
+ 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`);
69
+ }
70
+ await json(join(raiz, ".codex", "hooks.json"), doc => {
71
+ const h = objeto(doc.hooks);
72
+ doc.hooks = h;
73
+ grupo(h, "SessionStart", "dd-harness sessao --host codex");
74
+ grupo(h, "PreToolUse", "dd-harness guarda --host codex");
75
+ });
76
+ avisos.push("Codex: abra /hooks e confira a confiança dos hooks; arquivos instalados não comprovam ativação.");
77
+ }
78
+ else {
79
+ await json(join(raiz, host === "claude" ? ".mcp.json" : ".agents/mcp_config.json"), doc => {
80
+ const servidores = objeto(doc.mcpServers);
81
+ doc.mcpServers = servidores;
82
+ if (servidores["dd-harness"])
83
+ avisos.push(`${host}: MCP existente preservado; confira a raiz no diagnóstico.`);
84
+ else
85
+ servidores["dd-harness"] = mcp;
86
+ });
87
+ if (host === "claude") {
88
+ await json(join(raiz, ".claude/settings.json"), doc => {
89
+ const h = objeto(doc.hooks);
90
+ doc.hooks = h;
91
+ grupo(h, "SessionStart", "dd-harness sessao --host claude", ["dd-harness politica --hook"]);
92
+ grupo(h, "PreToolUse", "dd-harness guarda --host claude", ["dd-harness cinto"]);
93
+ });
94
+ }
95
+ else {
96
+ await json(join(raiz, ".agents/hooks.json"), doc => {
97
+ const bloco = objeto(doc["dd-harness"]);
98
+ const handlers = bloco.PreInvocation ?? [];
99
+ if (!Array.isArray(handlers))
100
+ throw new Error("PreInvocation precisa ser lista; configuração preservada");
101
+ const comando = "dd-harness sessao --host antigravity";
102
+ if (doc["dd-harness"] && !handlers.some(h => h?.command === comando)) {
103
+ throw new Error("hook dd-harness já existe com conteúdo alheio; preservado");
104
+ }
105
+ bloco.PreInvocation = [...handlers.filter(h => h?.command !== comando), { type: "command", command: comando, timeout: 60 }];
106
+ grupo(bloco, "PreToolUse", "dd-harness guarda --host antigravity");
107
+ doc["dd-harness"] = bloco;
108
+ });
109
+ const regra = join(raiz, ".agents/rules/dd-harness.md");
110
+ const atual = await readFile(regra, "utf8").catch((e) => {
111
+ if (e.code === "ENOENT")
112
+ return null;
113
+ throw e;
114
+ });
115
+ const conteudo = "---\ntrigger: always_on\n---\n\n" + SUGESTAO_AGENTS;
116
+ if (atual === null)
117
+ await escreveAtomico(regra, conteudo);
118
+ else if (atual !== conteudo)
119
+ avisos.push("Antigravity: regra local existente preservada; revise a entrada de protocolo.");
120
+ }
121
+ }
122
+ }
123
+ await mkdir(dirname(join(raiz, "AGENTS.md")), { recursive: true });
124
+ await escrevePonteiro(raiz, "AGENTS.md");
125
+ if (hosts.includes("claude"))
126
+ await escrevePonteiro(raiz, "CLAUDE.md");
127
+ await escreveAtomico(join(estadoDoRepo(raiz), "hosts.json"), JSON.stringify([...new Set([...await hostsInstalados(raiz), ...hosts])]));
128
+ return avisos;
129
+ }
package/dist/index.js CHANGED
@@ -1,7 +1,12 @@
1
1
  #!/usr/bin/env node
2
+ import { diagnostico } from "./diagnostico.js";
3
+ import { instalaHosts, selecionaHosts } from "./hosts.js";
4
+ import { achaRaiz } from "./config.js";
5
+ import { abreSessao, guardaSessao, saidaDaGuarda, saidaDoBoot } from "./sessao.js";
2
6
  import { spawn } from "node:child_process";
3
7
  import { access, readFile } from "node:fs/promises";
4
- import { join } from "node:path";
8
+ import { join, resolve } from "node:path";
9
+ const DIRETORIO_DA_INVOCACAO = process.cwd();
5
10
  // DEP0190: Node imprime esse warning direto no stderr no instante em que
6
11
  // `spawn(..., { shell: true })` recebe um array de args — de forma sincrona, ANTES de
7
12
  // qualquer listener rodar, entao filtrar dentro de um `process.on("warning")` normal nao
@@ -14,10 +19,9 @@ process.removeAllListeners("warning");
14
19
  import { termosDaConsulta } from "./argv.js";
15
20
  import { check, status } from "./check.js";
16
21
  import { CAMINHO_CONFIG, guardaConfigDaMaquina, guardaToken, leConfigDaMaquina, leConfigDoRepo, leToken, } from "./config.js";
17
- import { escreveHook, escreveMcp, escrevePonteiro } from "./escreve-config.js";
18
22
  import { escreveGanchoDeCommit, iniciaRepositorio } from "./git.js";
19
23
  import { grava } from "./gravar.js";
20
- import { init, SUGESTAO_AGENTS, SUGESTAO_HOOK, SUGESTAO_MCP } from "./init.js";
24
+ import { init } from "./init.js";
21
25
  import { pergunta, escolha, fechaPerguntas } from "./pergunta.js";
22
26
  import { buscaPolitica } from "./politica.js";
23
27
  import { avisoDeOrdem, blocoDeSessao, criaFase, editaFase, formataChangelog, formataRoadmap, leFases, } from "./roadmap.js";
@@ -30,8 +34,9 @@ import { criaPasta } from "./pasta.js";
30
34
  import { criaProjeto, listaTenants } from "./projeto.js";
31
35
  import { reancora } from "./reancorar.js";
32
36
  import { subiuOWorker } from "./worker.js";
37
+ import { REGRA_DE_COMMITS } from "./regras-de-commit.js";
33
38
  /**
34
- * `dd-harness` — o cliente que materializa os artefatos no repositorio.
39
+ * `dd-harness` — o cliente de política, memória e integrações de hosts.
35
40
  *
36
41
  * Sem dependencia de proposito: `fetch`, `crypto` e `fs` sao do Node, e o molde original
37
42
  * ja seguia a regra de validador determinístico sem dependencia. Um CLI que vive pinado
@@ -40,14 +45,18 @@ import { subiuOWorker } from "./worker.js";
40
45
  */
41
46
  const AJUDA = `dd-harness — a política e o Brain do projeto, no serviço
42
47
 
43
- dd-harness start numa pasta vazia: conduz tudo (login,
48
+ dd-harness diagnostico [--mcp] confere projeto, contexto e conexão MCP
49
+ dd-harness integrar --host todos instala MCP/regras/hooks em projeto existente
50
+ dd-harness sessao --host <host> hook de boot (stdin JSON)
51
+ dd-harness guarda --host <host> hook antes de ferramentas (stdin JSON)
52
+ dd-harness start [--host <host>] numa pasta vazia: conduz tudo (login,
44
53
  espaço, projeto, config) numa tacada
45
54
  dd-harness login --token <token> [--api <url>]
46
55
  guarda a credencial desta máquina
47
56
  dd-harness projeto <slug> --nome "<nome>" [--tenant <t>] [--api <url>]
48
57
  cria o projeto no serviço (antes do init)
49
58
  dd-harness init --tenant <t> --projeto <p> [--api <url>]
50
- prepara o repositório (config + CLAUDE.md)
59
+ escreve .dd-harness.json; depois rode integrar
51
60
  dd-harness pasta <slug> --definicao "o que entra e o que não entra"
52
61
  cria a pasta que o gravar exige
53
62
  dd-harness gravar <arquivo.md> registra uma memória a partir de um markdown
@@ -94,8 +103,9 @@ const AJUDA = `dd-harness — a política e o Brain do projeto, no serviço
94
103
  dd-harness --help
95
104
  dd-harness --version qual binário está instalado nesta máquina
96
105
 
97
- Nada do dd-harness fica em disco: a política chega pelo hook de sessão, e a
98
- memória pela busca, na hora.
106
+ Política e briefing chegam do serviço. O disco guarda configuração, ponteiros
107
+ para skills e estado descartável em ~/.dd-harness. Hosts: claude, codex,
108
+ antigravity, lista separada por vírgulas ou todos.
99
109
  `;
100
110
  /**
101
111
  * Os ganchos sao IMPRESSOS, nunca instalados. `.git/hooks` nao e versionado e nao e
@@ -111,8 +121,7 @@ Opcional — o gancho que devolve a memória ao code review:
111
121
 
112
122
  Termina em sucesso mesmo com deriva: avisa, não bloqueia.
113
123
 
114
- O hook da política (\`dd-harness politica --hook\`) é outra coisa, e não é
115
- opcional — \`dd-harness init\` imprime a linha para o \`.claude/settings.json\`.`;
124
+ Instale os hooks de sessão e guarda com \`dd-harness integrar --host <host>\`.`;
116
125
  function argumento(argv, nome) {
117
126
  const i = argv.indexOf(`--${nome}`);
118
127
  return i >= 0 ? argv[i + 1] : undefined;
@@ -173,7 +182,7 @@ function statusDoArgv(argv) {
173
182
  /** `--conteudo` aponta para um .md, como `gravar` e `editar` — markdown de verdade nao cabe em flag. */
174
183
  async function conteudoDoArgv(argv) {
175
184
  const caminho = argumento(argv, "conteudo");
176
- return caminho === undefined ? undefined : readFile(caminho, "utf8");
185
+ return caminho === undefined ? undefined : readFile(resolve(DIRETORIO_DA_INVOCACAO, caminho), "utf8");
177
186
  }
178
187
  /**
179
188
  * `fase criar` e `fase editar`. Concluir uma fase e `fase editar <slug> --status concluida`
@@ -338,7 +347,8 @@ function mascara(token) {
338
347
  * tomada de proposito aqui, ao contrario de `init`, que continua so sugerindo. O preco e
339
348
  * o merge cuidadoso em `escreve-config.ts`: nunca perder o que a pessoa ja tinha.
340
349
  */
341
- async function comandoStart() {
350
+ async function comandoStart(argv) {
351
+ const hosts = selecionaHosts(argumento(argv, "host"));
342
352
  const caminhoConfig = join(process.cwd(), CAMINHO_CONFIG);
343
353
  try {
344
354
  await access(caminhoConfig);
@@ -452,54 +462,13 @@ async function comandoStart() {
452
462
  // 4. Config do repositorio — o unico arquivo que `init` tambem escreveria.
453
463
  const r = await init(process.cwd(), { tenant, projeto: resultadoProjeto.projeto, api: API_PADRAO });
454
464
  console.log(r.config === "criada" ? `criado ${CAMINHO_CONFIG}` : `mantido ${CAMINHO_CONFIG}`);
455
- // 5. Hook e MCP — ESCRITOS, com merge. E aqui que `start` diverge de `init`.
456
- const hook = await escreveHook(process.cwd());
457
- if (!hook.ok) {
458
- console.log(`\nAVISO: ${hook.caminho} não é um JSON válido — não toquei nele. ` +
459
- "Acrescente manualmente:\n");
460
- console.log(SUGESTAO_HOOK);
461
- }
462
- else {
463
- console.log({ criado: "criado .claude/settings.json (hook adicionado)",
464
- "ja-tinha": "mantido .claude/settings.json — hook já estava lá",
465
- acrescentado: "atualizado .claude/settings.json (hook adicionado ao que já existia)",
466
- }[hook.estado]);
467
- }
468
- const mcp = await escreveMcp(process.cwd());
469
- if (!mcp.ok) {
470
- console.log(`\nAVISO: ${mcp.caminho} não é um JSON válido — não toquei nele. ` +
471
- "Acrescente manualmente:\n");
472
- console.log(SUGESTAO_MCP);
473
- }
474
- else {
475
- console.log({ criado: "criado .mcp.json (servidor dd-harness adicionado)",
476
- "ja-tinha": "mantido .mcp.json — servidor já estava declarado",
477
- acrescentado: "atualizado .mcp.json (servidor adicionado ao que já existia)",
478
- }[mcp.estado]);
479
- }
480
- if (mcp.ok) {
481
- console.log("\nInstalando a versão mais recente do servidor dd-harness-mcp " +
482
- "(a primeira vez pode demorar um pouco)...");
483
- const { ok: instalou, erro: erroInstalacao } = await instalaMcp();
484
- console.log(instalou
485
- ? "dd-harness-mcp instalado — a próxima sessão do Claude Code conecta na hora."
486
- : `AVISO: não consegui pré-instalar dd-harness-mcp (${erroInstalacao}). A primeira ` +
487
- "conexão do Claude Code pode demorar ou falhar; se falhar, abra uma nova sessão " +
488
- "e tente de novo.");
489
- }
490
- // 6. CLAUDE.md e AGENTS.md — os dois SEMPRE, sem perguntar. Sao espelhos: o mesmo
491
- // apontamento para o servico, cada um na porta que uma familia de ferramenta le. A
492
- // pergunta "usa outra ferramenta?" saiu porque a resposta nao muda nada que valha a
493
- // pena — quem responde "nao" hoje abre o repositorio no Cursor semana que vem e nao
494
- // encontra protocolo nenhum, e o custo de ja ter o arquivo e um ponteiro de 8 linhas.
495
- console.log("");
496
- for (const arquivo of ["CLAUDE.md", "AGENTS.md"]) {
497
- const r = await escrevePonteiro(process.cwd(), arquivo);
498
- console.log({ criado: `criado ${arquivo}`,
499
- "ja-tinha": `mantido ${arquivo} — já apontava para a política`,
500
- acrescentado: `atualizado ${arquivo} (apontamento acrescentado ao que já existia)`,
501
- }[r.estado]);
502
- }
465
+ const avisos = await instalaHosts(process.cwd(), hosts);
466
+ console.log('Integrações instaladas: ' + hosts.join(', '));
467
+ for (const aviso of avisos)
468
+ console.log('AVISO: ' + aviso);
469
+ const instalacao = await instalaMcp();
470
+ if (!instalacao.ok)
471
+ console.log('AVISO: MCP não pré-instalado; execute diagnostico antes de usar.');
503
472
  // 6.5. Os ponteiros das skills do projeto.
504
473
  //
505
474
  // O Claude Code descobre skill lendo `.claude/skills/*/SKILL.md` na ABERTURA da sessao,
@@ -518,7 +487,9 @@ async function comandoStart() {
518
487
  }
519
488
  const skills = await leSkills(process.cwd());
520
489
  if (skills.length > 0) {
521
- const { escritos } = await escrevePonteirosDeSkills(process.cwd(), skills);
490
+ const { escritos, conflitos } = await escrevePonteirosDeSkills(process.cwd(), skills, hosts.map(h => h === "claude" ? ".claude" : ".agents"));
491
+ if (conflitos.length)
492
+ console.log("Ponteiros preservados por conflito: " + conflitos.join(", "));
522
493
  console.log(escritos > 0
523
494
  ? `escritos ${escritos} ponteiro(s) de skill em .claude/skills/`
524
495
  : `${skills.length} skill(s) — ponteiros já estavam em dia`);
@@ -558,38 +529,7 @@ async function comandoInit(argv) {
558
529
  });
559
530
  console.log(r.config === "criada" ? "criado .dd-harness.json" : "mantido .dd-harness.json");
560
531
  console.log("\nAgora: dd-harness login --token <token>");
561
- // O hook vem primeiro e nao e opcional: sem ele a sessao abre sem politica, que e a
562
- // falha que este projeto existe para combater. O MCP e conveniencia; este, nao.
563
- if (r.hook === "ja-declarado") {
564
- console.log("\nmantido .claude/settings.json — o hook da política já está declarado");
565
- }
566
- else {
567
- console.log("\nOBRIGATÓRIO: o hook que carrega a política no início de cada sessão." +
568
- "\nSem ele a sessão abre sem protocolo, e nada avisa. Acrescente ao" +
569
- "\n`.claude/settings.json` (não escrevo nele: o arquivo é seu e pode já" +
570
- "\nter hooks e permissões):\n");
571
- console.log(SUGESTAO_HOOK);
572
- }
573
- if (r.mcp === "ja-declarado") {
574
- console.log("\nmantido .mcp.json — o servidor dd-harness já está declarado");
575
- }
576
- else {
577
- console.log("\nOpcional: as memórias como ferramenta, para o agente buscar e gravar sem" +
578
- "\nescrever arquivo. Acrescente ao `.mcp.json` da raiz (não escrevo nele: o" +
579
- "\narquivo é seu e pode declarar outros servidores):\n");
580
- console.log(SUGESTAO_MCP);
581
- }
582
- // Fora do Claude Code o hook nao roda, e ai o MCP deixa de ser conveniencia: e o unico
583
- // caminho da politica. Mas ferramenta disponivel nao e ferramenta chamada — sem esta
584
- // instrucao, o agente pode nunca perguntar.
585
- if (r.agents === "ja-aponta") {
586
- console.log("\nmantido AGENTS.md — já manda buscar a política pelo MCP");
587
- return;
588
- }
589
- console.log("\nUsa Codex, Cursor, Gemini CLI ou Windsurf? Eles NÃO rodam o hook do" +
590
- "\nClaude Code, e sem isto a sessão abre sem política. Crie um `AGENTS.md`" +
591
- "\nna raiz (ou acrescente ao seu) com:\n");
592
- console.log(SUGESTAO_AGENTS);
532
+ console.log("\nConfigure MCP, hooks e ponteiros: dd-harness integrar --host claude,codex,antigravity");
593
533
  }
594
534
  /**
595
535
  * Nao exige `.dd-harness.json`, e isso importa: sem projeto no servico o `init` nao tem o
@@ -612,7 +552,7 @@ async function comandoEditar(argv) {
612
552
  if (!caminho || caminho.startsWith("-")) {
613
553
  throw new Error("uso: dd-harness editar <arquivo.md>");
614
554
  }
615
- const r = await edita(process.cwd(), caminho);
555
+ const r = await edita(process.cwd(), resolve(DIRETORIO_DA_INVOCACAO, caminho));
616
556
  console.log(`editado ${r.endereco}`);
617
557
  console.log(` ${r.ancoras} âncora(s).`);
618
558
  }
@@ -993,6 +933,7 @@ function contextoDaSessao(r, worker) {
993
933
  "`reset --hard`, reescrever história publicada. Ficam fora por padrão.\n" +
994
934
  " Escreva cada resposta como regra. \"Ele sabe o que pode\" não sobrevive à " +
995
935
  "próxima sessão, que começa sem memória disto.\n" +
936
+ " Inclua também esta convenção nos commits:\n" + REGRA_DE_COMMITS + "\n\n" +
996
937
  "4. Mostre o rascunho da política e do briefing e espere a confirmação " +
997
938
  "explícita do usuário antes de escrever.\n" +
998
939
  "5. Escreva os dois com a ferramenta MCP `escrever_artefato` " +
@@ -1164,7 +1105,7 @@ async function comandoGravar(argv) {
1164
1105
  if (!caminho || caminho.startsWith("-")) {
1165
1106
  throw new Error("uso: dd-harness gravar <arquivo.md>");
1166
1107
  }
1167
- const r = await grava(process.cwd(), caminho);
1108
+ const r = await grava(process.cwd(), resolve(DIRETORIO_DA_INVOCACAO, caminho));
1168
1109
  console.log(`gravado ${r.endereco}`);
1169
1110
  console.log(` ${r.ancoras} âncora(s), valendo para ${r.projetos} projeto(s).`);
1170
1111
  }
@@ -1199,9 +1140,12 @@ async function principal() {
1199
1140
  console.log(AJUDA);
1200
1141
  return;
1201
1142
  }
1143
+ if (comando && !["start", "init", "login", "projeto", "sessao", "guarda", "--help", "-h", "--version", "-V", "version"].includes(comando)) {
1144
+ process.chdir(await achaRaiz());
1145
+ }
1202
1146
  switch (comando) {
1203
1147
  case "start":
1204
- return comandoStart();
1148
+ return comandoStart(resto);
1205
1149
  case "init":
1206
1150
  return comandoInit(resto);
1207
1151
  case "login":
@@ -1232,6 +1176,35 @@ async function principal() {
1232
1176
  return comandoCheck(resto);
1233
1177
  case "status":
1234
1178
  return comandoStatus();
1179
+ case "diagnostico":
1180
+ console.log((await diagnostico(process.cwd(), resto.includes("--mcp"))).join("\n"));
1181
+ break;
1182
+ case "integrar": {
1183
+ const hosts = selecionaHosts(argumento(resto, "host") ?? "todos");
1184
+ for (const aviso of await instalaHosts(process.cwd(), hosts))
1185
+ console.log(aviso);
1186
+ console.log("Integrações configuradas: " + hosts.join(", "));
1187
+ break;
1188
+ }
1189
+ case "sessao":
1190
+ case "guarda": {
1191
+ const host = selecionaHosts(argumento(resto, "host"))[0];
1192
+ try {
1193
+ let cru = "";
1194
+ for await (const chunk of process.stdin) {
1195
+ cru += chunk;
1196
+ if (cru.length > 2000000)
1197
+ throw new Error("Entrada de hook grande demais");
1198
+ }
1199
+ const entrada = JSON.parse(cru);
1200
+ console.log(JSON.stringify(comando === "sessao" ? await abreSessao(host, entrada) : await guardaSessao(host, entrada)));
1201
+ }
1202
+ catch (e) {
1203
+ const motivo = e instanceof Error ? e.message : String(e);
1204
+ console.log(JSON.stringify(comando === "guarda" ? saidaDaGuarda(host, motivo) : saidaDoBoot(host, motivo, false)));
1205
+ }
1206
+ break;
1207
+ }
1235
1208
  case "politica":
1236
1209
  return comandoPolitica(resto);
1237
1210
  case "cinto":
package/dist/pasta.js CHANGED
@@ -1,4 +1,4 @@
1
- import { cabecalhos, credencial, pede, recusa } from "./api.js";
1
+ import { jsonDaEscrita, 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
  *
@@ -25,6 +25,6 @@ export async function criaPasta(raiz, slug, definicao) {
25
25
  return { pasta: slug, jaExistia: true };
26
26
  if (!resposta.ok)
27
27
  await recusa(resposta);
28
- const { pasta } = (await resposta.json());
28
+ const { pasta } = (await jsonDaEscrita(resposta));
29
29
  return { pasta, jaExistia: false };
30
30
  }
package/dist/politica.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { cabecalhos, credencial, pede } from "./api.js";
2
+ import { politicaParaSessao } from "./regras-de-commit.js";
2
3
  export async function buscaPolitica(raiz) {
3
4
  let config;
4
5
  try {
@@ -29,10 +30,11 @@ export async function buscaPolitica(raiz) {
29
30
  // obrigatorios. So checar `politica` vazia deixava passar o caso de politica
30
31
  // existir sem briefing — a sessao seguia como "ok" com metade do briefing faltando,
31
32
  // e nada acionava o fluxo que devia completa-lo.
32
- if (!conteudo || payload.briefado === false) {
33
+ if (!conteudo || payload.briefado === false || !payload.briefing?.trim()) {
33
34
  return { estado: "sem-politica", esperandoIndexacao, memorias: payload.memorias };
34
35
  }
35
- return { estado: "ok", conteudo, esperandoIndexacao, roadmap, memorias: payload.memorias };
36
+ return { estado: "ok", conteudo: politicaParaSessao(conteudo) + "\n\n# Briefing do projeto\n\n" + payload.briefing,
37
+ esperandoIndexacao, roadmap, memorias: payload.memorias };
36
38
  }
37
39
  catch (erro) {
38
40
  return { estado: "inalcancavel", motivo: mensagem(erro) };
package/dist/projeto.js CHANGED
@@ -1,4 +1,4 @@
1
- import { cabecalhos, pede, recusa } from "./api.js";
1
+ import { jsonDaEscrita, cabecalhos, pede, recusa } from "./api.js";
2
2
  import { leConfigDoRepo, leToken } from "./config.js";
3
3
  /**
4
4
  * `dd-harness projeto <slug> --nome "..."` — cria o projeto sem sair da sessao.
@@ -34,7 +34,7 @@ export async function criaProjeto(raiz, slug, nome, explicito = {}) {
34
34
  return { projeto: slug, jaExistia: true };
35
35
  if (!resposta.ok)
36
36
  await recusa(resposta);
37
- const { projeto } = (await resposta.json());
37
+ const { projeto } = (await jsonDaEscrita(resposta));
38
38
  return { projeto, jaExistia: false };
39
39
  }
40
40
  /**
@@ -0,0 +1,3 @@
1
+ /** Regra do produto, entregue junto da política sem reescrever o artefato no serviço. */
2
+ export declare const REGRA_DE_COMMITS = "## Identifica\u00E7\u00E3o do agente nos commits\n\nTodo commit feito por agente come\u00E7a com [IDENTIFICA\u00C7\u00C3O] - tipo: descri\u00E7\u00E3o.\nExemplos: [CODEX] - feat: nova funcionalidade; [CLAUDE] - fix: corrige a busca;\n[GEMINI] - docs: atualiza o guia. Outro agente usa seu nome em mai\u00FAsculas entre colchetes.\nIdentifique o agente/modelo que efetivamente realizou o commit, n\u00E3o apenas o editor:\nClaude no Antigravity continua [CLAUDE]; Gemini usa [GEMINI]; Codex usa [CODEX].\nN\u00E3o invente a identidade. A regra n\u00E3o concede autoriza\u00E7\u00E3o para commitar, n\u00E3o altera\no autor do Git e n\u00E3o pede reescrita de commits antigos.";
3
+ export declare function politicaParaSessao(conteudo: string): string;