dd-harness 0.31.0 → 0.32.1

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,15 @@
1
+ /** Regra do produto, entregue junto da política sem reescrever o artefato no serviço. */
2
+ export const REGRA_DE_COMMITS = `## Identificação do agente nos commits
3
+
4
+ Todo commit feito por agente começa com [IDENTIFICAÇÃO] - tipo: descrição.
5
+ Exemplos: [CODEX] - feat: nova funcionalidade; [CLAUDE] - fix: corrige a busca;
6
+ [GEMINI] - docs: atualiza o guia. Outro agente usa seu nome em maiúsculas entre colchetes.
7
+ Identifique o agente/modelo que efetivamente realizou o commit, não apenas o editor:
8
+ Claude no Antigravity continua [CLAUDE]; Gemini usa [GEMINI]; Codex usa [CODEX].
9
+ Não invente a identidade. A regra não concede autorização para commitar, não altera
10
+ o autor do Git e não pede reescrita de commits antigos.`;
11
+ export function politicaParaSessao(conteudo) {
12
+ if (!conteudo.trim() || conteudo.includes(REGRA_DE_COMMITS))
13
+ return conteudo;
14
+ return `${conteudo}\n\n---\n\n${REGRA_DE_COMMITS}`;
15
+ }
package/dist/roadmap.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
  // --- rede ---
3
3
  export async function leFases(raiz, filtro = {}) {
4
4
  const { config, token } = await credencial(raiz);
@@ -33,7 +33,7 @@ export async function criaFase(raiz, dados) {
33
33
  });
34
34
  if (!resposta.ok)
35
35
  await recusa(resposta);
36
- return (await resposta.json());
36
+ return (await jsonDaEscrita(resposta));
37
37
  }
38
38
  export async function editaFase(raiz, slug, dados) {
39
39
  const { config, token } = await credencial(raiz);
@@ -44,7 +44,7 @@ export async function editaFase(raiz, slug, dados) {
44
44
  });
45
45
  if (!resposta.ok)
46
46
  await recusa(resposta);
47
- return (await resposta.json());
47
+ return (await jsonDaEscrita(resposta));
48
48
  }
49
49
  // --- formatacao (pura) ---
50
50
  /** Rotulo do grupo de fases concluidas sem versao: o "Unreleased" do Keep a Changelog. */
@@ -0,0 +1,5 @@
1
+ import { type Host } from "./edicoes.js";
2
+ export declare function saidaDoBoot(host: Host, contexto: string, ok: boolean): object;
3
+ export declare function abreSessao(host: Host, entrada: Record<string, unknown>): Promise<object>;
4
+ export declare function saidaDaGuarda(host: Host, motivo?: string, contexto?: string): object;
5
+ export declare function guardaSessao(host: Host, entrada: Record<string, unknown>): Promise<object>;
package/dist/sessao.js ADDED
@@ -0,0 +1,129 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { credencial } from "./api.js";
5
+ import { achaRaiz } from "./config.js";
6
+ import { decideDoHook, guardaCache, leCache } from "./cinto.js";
7
+ import { edicoesDaChamada, normalizaEntrada } from "./edicoes.js";
8
+ import { estadoDoRepo, escreveAtomico, revisaoDoContexto } from "./estado-local.js";
9
+ import { buscaPolitica } from "./politica.js";
10
+ import { blocoDeSessao } from "./roadmap.js";
11
+ import { leSkills, escrevePonteirosDeSkills } from "./skill.js";
12
+ import { REGRA_DE_COMMITS } from "./regras-de-commit.js";
13
+ import { subiuOWorker } from "./worker.js";
14
+ const PRAZO = 4 * 60 * 60 * 1000;
15
+ const hash = (s) => createHash("sha256").update(s).digest("hex");
16
+ const arquivo = (raiz, sessao, host) => join(estadoDoRepo(raiz), `sessao-${hash(host + sessao)}.json`);
17
+ async function identidade(raiz) {
18
+ const c = await credencial(raiz);
19
+ return hash(JSON.stringify(c)); // inclui token sem expô-lo em disco/log.
20
+ }
21
+ async function estadoAtual(raiz, sessao, host) {
22
+ try {
23
+ const e = JSON.parse(await readFile(arquivo(raiz, sessao, host), "utf8"));
24
+ if (e.estado === "ok" && e.revisao !== await revisaoDoContexto(raiz))
25
+ return null;
26
+ return e.identidade === await identidade(raiz) && Date.now() >= e.em && Date.now() - e.em < PRAZO ? e : null;
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ export function saidaDoBoot(host, contexto, ok) {
33
+ return host === "antigravity"
34
+ ? { injectSteps: contexto ? [{ ephemeralMessage: contexto }] : [] }
35
+ : { ...(ok ? {} : { continue: false, stopReason: "dd-harness: contexto obrigatório indisponível" }),
36
+ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: contexto } };
37
+ }
38
+ export async function abreSessao(host, entrada) {
39
+ const c = normalizaEntrada(host, entrada);
40
+ if (!c.sessao)
41
+ throw new Error("hook sem identificador de sessão: não é possível validar o boot");
42
+ const raiz = await achaRaiz(c.raiz);
43
+ const anterior = await estadoAtual(raiz, c.sessao, host);
44
+ // A mensagem do Antigravity é efêmera: não presumir que sobreviva à próxima inferência.
45
+ const inicio = host !== "antigravity" || Number(entrada.invocationNum) === 0 || anterior?.estado !== "ok";
46
+ const ciclo = inicio ? randomUUID() : anterior.ciclo;
47
+ await escreveAtomico(arquivo(raiz, c.sessao, host), JSON.stringify({ estado: "inalcancavel", em: 0 }));
48
+ const id = await identidade(raiz).catch(() => "");
49
+ const revisao = await revisaoDoContexto(raiz);
50
+ const r = await buscaPolitica(raiz);
51
+ let contexto;
52
+ if (r.estado === "ok") {
53
+ contexto = r.conteudo + blocoDeSessao(r.roadmap);
54
+ await guardaCache(raiz, { memorias: r.memorias ?? [] });
55
+ if (inicio)
56
+ try {
57
+ const skills = await leSkills(raiz, AbortSignal.timeout(6000));
58
+ const sync = await escrevePonteirosDeSkills(raiz, skills, [host === "claude" ? ".claude" : ".agents"]);
59
+ if (skills.length)
60
+ contexto += "\n\nSkills disponíveis (use ler_skill para o procedimento):\n" +
61
+ skills.map(s => `- ${s.slug}${s.so_por_comando ? " (só por pedido explícito)" : ""}: ${s.descricao}`).join("\n");
62
+ if (sync.conflitos.length)
63
+ contexto += `\n\n${sync.conflitos.length} ponteiro(s) preservado(s) por conflito local; consulte listar_skills.`;
64
+ if (sync.ignoradas.length)
65
+ contexto += `\n\nO serviço não devolveu skill alguma, mas ${sync.ignoradas.length} ponteiro(s) ` +
66
+ `continuam em disco (${sync.ignoradas.join(", ")}); eles não foram apagados porque lista vazia costuma ser falha de leitura. ` +
67
+ `Confirme em listar_skills antes de tratar essas skills como removidas.`;
68
+ }
69
+ catch {
70
+ contexto += "\n\nNão foi possível atualizar as skills; consulte listar_skills quando o serviço voltar.";
71
+ }
72
+ if (r.esperandoIndexacao && inicio) {
73
+ const worker = await subiuOWorker(raiz, r.esperandoIndexacao);
74
+ contexto += `\n\n${r.esperandoIndexacao} memória(s) aguardam indexação; a busca pode estar incompleta. ` +
75
+ (worker.subiu ? `Lote solicitado; isto não comprova conclusão. Consulte status e o log ${worker.log}.` :
76
+ "Worker não iniciado; execute o worker configurado e confira status.");
77
+ }
78
+ }
79
+ else if (r.estado === "sem-politica") {
80
+ contexto = "Projeto sem política e/ou briefing. Conduza o briefing com o usuário e, após aprovação, " +
81
+ "grave os dois com escrever_artefato. Defina permissões de commit e push separadamente. " +
82
+ "Depois consulte ler_artefato para verificar e reabra a sessão. Escritas no código permanecem bloqueadas.\n\n" + REGRA_DE_COMMITS;
83
+ }
84
+ else
85
+ contexto = `PARE: não foi possível carregar a política e o briefing. ${r.motivo}. Corrija a conexão/configuração e reabra a sessão.`;
86
+ await escreveAtomico(arquivo(raiz, c.sessao, host), JSON.stringify({ identidade: id,
87
+ revisao, ciclo, estado: r.estado, em: Date.now() }));
88
+ return saidaDoBoot(host, contexto, r.estado !== "inalcancavel");
89
+ }
90
+ export function saidaDaGuarda(host, motivo, contexto) {
91
+ if (host === "antigravity")
92
+ return { decision: motivo ? "deny" : "ask", reason: motivo ?? contexto ?? "" };
93
+ return { hookSpecificOutput: { hookEventName: "PreToolUse",
94
+ ...(motivo ? { permissionDecision: "deny", permissionDecisionReason: motivo } : {}),
95
+ ...(contexto ? { additionalContext: contexto } : {}) } };
96
+ }
97
+ // Só ferramentas conhecidas de leitura passam quando o boot falta. Shell não é classificado por regex.
98
+ const LEITURA = /^(Read|Glob|Grep|view_file|list_dir|find_by_name|grep_search|ask_question|AskUserQuestion|request_user_input)$/;
99
+ const LEITURA_MCP = /^mcp__dd[-_]harness__(ler_artefato|ler_roadmap|ler_changelog|listar_skills|ler_skill|ler_memoria|buscar_memoria)$/;
100
+ export async function guardaSessao(host, entrada) {
101
+ const c = normalizaEntrada(host, entrada);
102
+ const raiz = await achaRaiz(c.raiz);
103
+ const e = c.sessao ? await estadoAtual(raiz, c.sessao, host) : null;
104
+ if (e?.estado !== "ok") {
105
+ const onboarding = e?.estado === "sem-politica" && /^mcp__dd[-_]harness__escrever_artefato$/.test(c.ferramenta);
106
+ if (LEITURA.test(c.ferramenta) || LEITURA_MCP.test(c.ferramenta) || onboarding)
107
+ return saidaDaGuarda(host);
108
+ return saidaDaGuarda(host, "dd-harness: política e briefing não foram carregados nesta sessão/projeto. Corrija o boot e reabra a sessão.");
109
+ }
110
+ const edicoes = edicoesDaChamada(c);
111
+ const avisos = [];
112
+ if (edicoes.length && !await leCache(raiz)) {
113
+ const r = await buscaPolitica(raiz);
114
+ if (r.estado !== "ok") {
115
+ await escreveAtomico(arquivo(raiz, c.sessao, host), JSON.stringify({ estado: "inalcancavel", em: 0 }));
116
+ return saidaDaGuarda(host, "dd-harness: falha ao revalidar contexto após mudança. Reabra a sessão.");
117
+ }
118
+ if (r.memorias)
119
+ await guardaCache(raiz, { memorias: r.memorias });
120
+ }
121
+ for (const edicao of edicoes) {
122
+ const depois = edicao.origem ? await readFile(edicao.origem, "utf8").catch(() => undefined) : edicao.depois;
123
+ const r = JSON.parse(await decideDoHook({ cwd: raiz, session_id: host + c.sessao + e.ciclo,
124
+ tool_input: { file_path: edicao.arquivo, old_string: edicao.antes, new_string: depois } }));
125
+ if (r.hookSpecificOutput.additionalContext)
126
+ avisos.push(r.hookSpecificOutput.additionalContext);
127
+ }
128
+ return saidaDaGuarda(host, undefined, avisos.join("\n\n"));
129
+ }
package/dist/skill.d.ts CHANGED
@@ -26,7 +26,7 @@ export type Skill = {
26
26
  export type SkillCompleta = Skill & {
27
27
  conteudo: string;
28
28
  };
29
- export declare function leSkills(raiz: string): Promise<Skill[]>;
29
+ export declare function leSkills(raiz: string, signal?: AbortSignal): Promise<Skill[]>;
30
30
  export declare function leSkill(raiz: string, slug: string): Promise<SkillCompleta>;
31
31
  export declare function criaSkill(raiz: string, dados: NovaSkillInicial & Partial<Pick<Skill, "so_por_comando" | "ferramentas" | "caminhos" | "dica_de_argumento">>): Promise<{
32
32
  skill: Skill;
@@ -38,17 +38,24 @@ export declare function criaSkill(raiz: string, dados: NovaSkillInicial & Partia
38
38
  * pediria permissao para ler a propria skill, e um "permitir?" antes de cada invocacao
39
39
  * ensina a desligar o mecanismo.
40
40
  */
41
- export declare function ponteiroDaSkill(skill: Skill): string;
41
+ export declare function ponteiroDaSkill(skill: Skill, formato?: "claude" | "padrao"): string;
42
42
  /**
43
- * Escreve os ponteiros de todas as skills do projeto.
43
+ * Atualiza/remove somente ponteiros registrados e intactos. Preserva arquivos manuais,
44
+ * edições locais e recursos adicionais nos diretórios de skills.
44
45
  *
45
- * NAO apaga diretorio que sobrou: uma skill apagada no servico deixa o ponteiro orfao, e
46
- * varrer `.claude/skills/` inteiro levaria junto as skills que a pessoa escreveu a mao —
47
- * que sao legitimas e nao passam por aqui. O DELETE da API avisa o que apagar; a decisao
48
- * de apagar arquivo alheio nao e nossa.
46
+ * **Lista vazia nunca remove.** "O serviço não tem skills" e "a leitura falhou" chegam aqui
47
+ * com a mesma cara — lista sem itens —, mas erram em direções muito diferentes: preservar
48
+ * um ponteiro obsoleto custa uma skill desatualizada até o próximo boot; apagar todos custa
49
+ * as skills inteiras, em silêncio, no projeto de quem só abriu o editor. Remoção real vem
50
+ * uma de cada vez, e nesse caso sobra alguma skill na lista. O acervo inteiro sumindo de uma
51
+ * vez é sinal de problema, não de intenção — então os ponteiros ficam e voltam em
52
+ * `ignoradas`, para o chamador poder dizer o que houve.
49
53
  */
50
- export declare function escrevePonteirosDeSkills(raiz: string, skills: Skill[]): Promise<{
54
+ export declare function escrevePonteirosDeSkills(raiz: string, skills: Skill[], destinos?: (".claude" | ".agents")[]): Promise<{
51
55
  escritos: number;
56
+ removidos: number;
57
+ conflitos: string[];
58
+ ignoradas: string[];
52
59
  }>;
53
60
  /** O minimo para criar uma skill: o resto tem default no banco. */
54
61
  export type NovaSkillInicial = {
package/dist/skill.js CHANGED
@@ -1,13 +1,15 @@
1
- import { mkdir, readFile, writeFile } from "node:fs/promises";
1
+ import { createHash } from "node:crypto";
2
+ import { estadoDoRepo, escreveAtomico } from "./estado-local.js";
3
+ import { mkdir, readFile, rm, lstat } from "node:fs/promises";
2
4
  import { join } from "node:path";
3
- import { cabecalhos, credencial, pede, recusa } from "./api.js";
5
+ import { jsonDaEscrita, cabecalhos, credencial, pede, recusa } from "./api.js";
4
6
  // --- rede ---
5
- export async function leSkills(raiz) {
7
+ export async function leSkills(raiz, signal) {
6
8
  const { config, token } = await credencial(raiz);
7
9
  const url = new URL(`${config.api}/api/v1/skills`);
8
10
  url.searchParams.set("tenant", config.tenant);
9
11
  url.searchParams.set("projeto", config.projeto);
10
- const resposta = await pede(url, { headers: cabecalhos(token) });
12
+ const resposta = await pede(url, { headers: cabecalhos(token), signal });
11
13
  if (!resposta.ok)
12
14
  await recusa(resposta);
13
15
  return (await resposta.json()).skills;
@@ -34,7 +36,7 @@ dados) {
34
36
  });
35
37
  if (!resposta.ok)
36
38
  await recusa(resposta);
37
- return (await resposta.json());
39
+ return (await jsonDaEscrita(resposta));
38
40
  }
39
41
  // --- o ponteiro em disco (puro, para ser testado sem rede) ---
40
42
  /**
@@ -55,27 +57,28 @@ function comoEscalarYaml(texto) {
55
57
  * pediria permissao para ler a propria skill, e um "permitir?" antes de cada invocacao
56
58
  * ensina a desligar o mecanismo.
57
59
  */
58
- export function ponteiroDaSkill(skill) {
60
+ export function ponteiroDaSkill(skill, formato = "claude") {
59
61
  const frontmatter = [
60
62
  "---",
61
63
  `name: ${skill.slug}`,
62
64
  `description: ${comoEscalarYaml(skill.descricao)}`,
63
65
  ];
64
- if (skill.so_por_comando)
66
+ if (skill.so_por_comando && formato === "claude")
65
67
  frontmatter.push("disable-model-invocation: true");
66
68
  if (skill.dica_de_argumento) {
67
69
  frontmatter.push(`argument-hint: ${comoEscalarYaml(skill.dica_de_argumento)}`);
68
70
  }
69
71
  const ferramentas = ["mcp__dd-harness__ler_skill", ...skill.ferramentas];
70
- frontmatter.push(`allowed-tools: ${ferramentas.join(", ")}`);
71
- if (skill.caminhos.length > 0) {
72
+ if (formato === "claude")
73
+ frontmatter.push(`allowed-tools: ${ferramentas.join(", ")}`);
74
+ if (formato === "claude" && skill.caminhos.length > 0) {
72
75
  frontmatter.push(`paths: ${skill.caminhos.join(", ")}`);
73
76
  }
74
77
  frontmatter.push("---");
75
78
  return `${frontmatter.join("\n")}
76
79
 
77
80
  # ${skill.slug}
78
-
81
+ ${skill.so_por_comando ? "\nSó execute quando o usuário pedir explicitamente esta skill.\n" : ""}
79
82
  O procedimento desta skill vive no dd-harness, não neste arquivo.
80
83
 
81
84
  **Chame a ferramenta MCP \`ler_skill\` com \`slug: "${skill.slug}"\` e siga o que ela
@@ -87,28 +90,111 @@ usuário em vez de improvisar um procedimento.
87
90
  `;
88
91
  }
89
92
  /**
90
- * Escreve os ponteiros de todas as skills do projeto.
93
+ * Atualiza/remove somente ponteiros registrados e intactos. Preserva arquivos manuais,
94
+ * edições locais e recursos adicionais nos diretórios de skills.
91
95
  *
92
- * NAO apaga diretorio que sobrou: uma skill apagada no servico deixa o ponteiro orfao, e
93
- * varrer `.claude/skills/` inteiro levaria junto as skills que a pessoa escreveu a mao —
94
- * que sao legitimas e nao passam por aqui. O DELETE da API avisa o que apagar; a decisao
95
- * de apagar arquivo alheio nao e nossa.
96
+ * **Lista vazia nunca remove.** "O serviço não tem skills" e "a leitura falhou" chegam aqui
97
+ * com a mesma cara — lista sem itens —, mas erram em direções muito diferentes: preservar
98
+ * um ponteiro obsoleto custa uma skill desatualizada até o próximo boot; apagar todos custa
99
+ * as skills inteiras, em silêncio, no projeto de quem só abriu o editor. Remoção real vem
100
+ * uma de cada vez, e nesse caso sobra alguma skill na lista. O acervo inteiro sumindo de uma
101
+ * vez é sinal de problema, não de intenção — então os ponteiros ficam e voltam em
102
+ * `ignoradas`, para o chamador poder dizer o que houve.
96
103
  */
97
- export async function escrevePonteirosDeSkills(raiz, skills) {
98
- let escritos = 0;
99
- for (const skill of skills) {
100
- const dir = join(raiz, ".claude", "skills", skill.slug);
101
- const caminho = join(dir, "SKILL.md");
102
- const novo = ponteiroDaSkill(skill);
103
- // Só escreve quando muda: reescrever igual sujaria o `git status` a cada `start`.
104
- const atual = await readFile(caminho, "utf8").catch(() => null);
105
- if (atual === novo)
106
- continue;
107
- await mkdir(dir, { recursive: true });
108
- await writeFile(caminho, novo, "utf8");
109
- escritos++;
104
+ export async function escrevePonteirosDeSkills(raiz, skills, destinos = [".claude"]) {
105
+ let escritos = 0, removidos = 0;
106
+ const conflitos = [];
107
+ const ignoradas = [];
108
+ const hash = (s) => createHash("sha256").update(s).digest("hex");
109
+ async function temLink(destino, slug) {
110
+ for (const caminho of [join(raiz, destino), join(raiz, destino, "skills"),
111
+ join(raiz, destino, "skills", slug), join(raiz, destino, "skills", slug, "SKILL.md")]) {
112
+ const st = await lstat(caminho).catch((e) => {
113
+ if (e.code === "ENOENT")
114
+ return null;
115
+ throw e;
116
+ });
117
+ if (st?.isSymbolicLink())
118
+ return true;
119
+ }
120
+ return false;
121
+ }
122
+ for (const destino of new Set(destinos)) {
123
+ const manifesto = join(estadoDoRepo(raiz), 'skills-' + destino.slice(1) + '.json');
124
+ let anterior = {};
125
+ try {
126
+ anterior = JSON.parse(await readFile(manifesto, "utf8"));
127
+ }
128
+ catch { /* Sem manifesto, arquivo existente pertence ao dono. */ }
129
+ const proximo = { ...anterior };
130
+ const presentes = new Set();
131
+ for (const skill of skills) {
132
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(skill.slug))
133
+ throw new Error("slug de skill inválido");
134
+ presentes.add(skill.slug);
135
+ // Hosts sem disable-model-invocation não recebem descoberta automática destas skills.
136
+ // O procedimento continua acessível por listar_skills/ler_skill quando o usuário pedir.
137
+ if (destino === ".agents" && skill.so_por_comando) {
138
+ presentes.delete(skill.slug);
139
+ continue;
140
+ }
141
+ const dir = join(raiz, destino, "skills", skill.slug);
142
+ const caminho = join(dir, "SKILL.md");
143
+ if (await temLink(destino, skill.slug)) {
144
+ conflitos.push(caminho);
145
+ continue;
146
+ }
147
+ const novo = ponteiroDaSkill(skill, destino === ".claude" ? "claude" : "padrao");
148
+ const atual = await readFile(caminho, "utf8").catch((e) => {
149
+ if (e.code === "ENOENT")
150
+ return null;
151
+ throw e;
152
+ });
153
+ if (atual !== null && hash(atual) !== anterior[skill.slug]) {
154
+ conflitos.push(caminho);
155
+ continue;
156
+ }
157
+ if (atual !== novo) {
158
+ await mkdir(dir, { recursive: true });
159
+ await escreveAtomico(caminho, novo);
160
+ escritos++;
161
+ }
162
+ proximo[skill.slug] = hash(novo);
163
+ }
164
+ for (const [slug, assinatura] of Object.entries(anterior)) {
165
+ if (presentes.has(slug))
166
+ continue;
167
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug))
168
+ continue;
169
+ // O manifesto fica como está: no próximo boot com lista boa, o ponteiro é conferido
170
+ // e removido normalmente. Esquecê-lo aqui tornaria o arquivo órfão e intocável.
171
+ if (skills.length === 0) {
172
+ ignoradas.push(slug);
173
+ continue;
174
+ }
175
+ const caminho = join(raiz, destino, "skills", slug, "SKILL.md");
176
+ if (await temLink(destino, slug)) {
177
+ conflitos.push(caminho);
178
+ continue;
179
+ }
180
+ const atual = await readFile(caminho, "utf8").catch((e) => {
181
+ if (e.code === "ENOENT")
182
+ return null;
183
+ throw e;
184
+ });
185
+ if (atual !== null && hash(atual) !== assinatura) {
186
+ conflitos.push(caminho);
187
+ continue;
188
+ }
189
+ if (atual !== null) {
190
+ await rm(caminho);
191
+ removidos++;
192
+ }
193
+ delete proximo[slug];
194
+ }
195
+ await escreveAtomico(manifesto, JSON.stringify(proximo));
110
196
  }
111
- return { escritos };
197
+ return { escritos, removidos, conflitos, ignoradas };
112
198
  }
113
199
  /**
114
200
  * Semeia as skills iniciais — so num projeto que ainda nao tem nenhuma.
@@ -0,0 +1 @@
1
+ export declare function versaoDoPacote(url: string, nome: string): Promise<string>;
package/dist/versao.js ADDED
@@ -0,0 +1,18 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ export async function versaoDoPacote(url, nome) {
5
+ let dir = dirname(fileURLToPath(url));
6
+ for (;;) {
7
+ try {
8
+ const p = JSON.parse(await readFile(join(dir, "package.json"), "utf8"));
9
+ if (p.name === nome)
10
+ return p.version;
11
+ }
12
+ catch { /* Procura o pacote do bin, não assume a profundidade do build. */ }
13
+ const pai = dirname(dir);
14
+ if (pai === dir)
15
+ throw new Error(`package.json de ${nome} não encontrado`);
16
+ dir = pai;
17
+ }
18
+ }
package/dist/worker.d.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  export declare function achaOWorker(raiz: string): Promise<string | null>;
10
10
  export type Subida = {
11
11
  subiu: true;
12
+ log: string;
12
13
  } | {
13
14
  subiu: false;
14
15
  motivo: "sem-fila" | "sem-worker" | "falhou";
package/dist/worker.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { spawn } from "node:child_process";
2
- import { access } from "node:fs/promises";
2
+ import { access, mkdir, open } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { leConfigDaMaquina } from "./config.js";
5
+ import { diretorioDoUsuario } from "./estado-local.js";
5
6
  /**
6
7
  * Subir o worker de embeddings quando ha fila esperando.
7
8
  *
@@ -65,15 +66,25 @@ export async function subiuOWorker(raiz, esperando) {
65
66
  if (!repo)
66
67
  return { subiu: false, motivo: "sem-worker" };
67
68
  try {
68
- const filho = spawn("npx", ["pnpm@latest", "worker", "--uma-vez"], {
69
- cwd: repo,
70
- detached: true,
71
- stdio: "ignore",
72
- shell: process.platform === "win32",
73
- windowsHide: true,
74
- });
75
- filho.unref();
76
- return { subiu: true };
69
+ const dir = diretorioDoUsuario();
70
+ await mkdir(dir, { recursive: true });
71
+ const caminhoLog = join(dir, "worker.log");
72
+ const log = await open(caminhoLog, "a", 0o600);
73
+ try {
74
+ const filho = spawn("npx", ["pnpm@latest", "worker", "--uma-vez"], {
75
+ cwd: repo,
76
+ detached: true,
77
+ stdio: ["ignore", log.fd, log.fd],
78
+ shell: process.platform === "win32",
79
+ windowsHide: true,
80
+ });
81
+ await new Promise((resolve, reject) => { filho.once("spawn", resolve); filho.once("error", reject); });
82
+ filho.unref();
83
+ return { subiu: true, log: caminhoLog };
84
+ }
85
+ finally {
86
+ await log.close();
87
+ }
77
88
  }
78
89
  catch (erro) {
79
90
  return {
package/package.json CHANGED
@@ -1,44 +1,44 @@
1
- {
2
- "name": "dd-harness",
3
- "version": "0.31.0",
4
- "type": "module",
5
- "description": "Cliente do dd-harness: politica no inicio da sessao, e memoria por busca — nada em disco. Sem dependencia: fetch, crypto e fs sao do Node.",
6
- "license": "UNLICENSED",
7
- "author": "Diego Dias",
8
- "keywords": [
9
- "claude-code",
10
- "ai-agents",
11
- "memory",
12
- "brain",
13
- "cli"
14
- ],
15
- "homepage": "https://dd-harness.vercel.app",
16
- "repository": {
17
- "type": "git",
18
- "url": "git+https://github.com/diegodias93/dd-harness-online.git",
19
- "directory": "packages/cli"
20
- },
21
- "bin": {
22
- "dd-harness": "dist/index.js"
23
- },
24
- "//exports": "Aponta para `dist` porque quem IMPORTA isto em tempo de execucao e o Node, que nao executa TypeScript. Nao aponte para `src`: o Node segue os imports relativos de dentro do arquivo e tenta abrir `./api.js` ao lado do `.ts`, que nao existe. Quem consome no workspace e o `packages/mcp`, e ele compila o fonte do CLI junto (ver o tsconfig.build.json dele) em vez de depender deste campo — assim o build funciona num checkout limpo, sem `dist` previo.",
25
- "exports": {
26
- "./api": "./dist/api.js",
27
- "./buscar": "./dist/buscar.js",
28
- "./curar": "./dist/curar.js",
29
- "./gravar": "./dist/gravar.js",
30
- "./pasta": "./dist/pasta.js",
31
- "./projeto": "./dist/projeto.js"
32
- },
33
- "files": [
34
- "dist"
35
- ],
36
- "engines": {
37
- "node": ">=20"
38
- },
39
- "scripts": {
40
- "typecheck": "tsc -p . --noEmit",
41
- "build": "tsc -p tsconfig.build.json",
42
- "prepublishOnly": "npm run build"
43
- }
44
- }
1
+ {
2
+ "name": "dd-harness",
3
+ "version": "0.32.1",
4
+ "type": "module",
5
+ "description": "Política, memória e integrações de Claude Code, Codex e Antigravity. CLI sem dependências de runtime.",
6
+ "license": "UNLICENSED",
7
+ "author": "Diego Dias",
8
+ "keywords": [
9
+ "claude-code",
10
+ "ai-agents",
11
+ "memory",
12
+ "brain",
13
+ "cli"
14
+ ],
15
+ "homepage": "https://dd-harness.vercel.app",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/diegodias93/dd-harness-online.git",
19
+ "directory": "packages/cli"
20
+ },
21
+ "bin": {
22
+ "dd-harness": "dist/index.js"
23
+ },
24
+ "//exports": "Aponta para `dist` porque quem IMPORTA isto em tempo de execucao e o Node, que nao executa TypeScript. Nao aponte para `src`: o Node segue os imports relativos de dentro do arquivo e tenta abrir `./api.js` ao lado do `.ts`, que nao existe. Quem consome no workspace e o `packages/mcp`, e ele compila o fonte do CLI junto (ver o tsconfig.build.json dele) em vez de depender deste campo — assim o build funciona num checkout limpo, sem `dist` previo.",
25
+ "exports": {
26
+ "./api": "./dist/api.js",
27
+ "./buscar": "./dist/buscar.js",
28
+ "./curar": "./dist/curar.js",
29
+ "./gravar": "./dist/gravar.js",
30
+ "./pasta": "./dist/pasta.js",
31
+ "./projeto": "./dist/projeto.js"
32
+ },
33
+ "files": [
34
+ "dist"
35
+ ],
36
+ "engines": {
37
+ "node": ">=20.3.0"
38
+ },
39
+ "scripts": {
40
+ "typecheck": "tsc -p . --noEmit",
41
+ "build": "tsc -p tsconfig.build.json",
42
+ "prepublishOnly": "npm run build"
43
+ }
44
+ }