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.
- package/README.md +103 -64
- package/dist/api.d.ts +2 -0
- package/dist/api.js +29 -25
- package/dist/artefato.js +6 -2
- package/dist/cinto.d.ts +2 -0
- package/dist/cinto.js +29 -10
- package/dist/config.d.ts +4 -0
- package/dist/config.js +35 -6
- package/dist/curar.js +9 -4
- package/dist/diagnostico.d.ts +5 -0
- package/dist/diagnostico.js +92 -0
- package/dist/edicoes.d.ts +15 -0
- package/dist/edicoes.js +52 -0
- package/dist/escreve-config.d.ts +1 -0
- package/dist/escreve-config.js +9 -3
- package/dist/estado-local.d.ts +7 -0
- package/dist/estado-local.js +33 -0
- package/dist/gravar.js +5 -15
- package/dist/hosts.d.ts +6 -0
- package/dist/hosts.js +129 -0
- package/dist/index.js +69 -96
- package/dist/pasta.js +2 -2
- package/dist/politica.js +4 -2
- package/dist/projeto.js +2 -2
- package/dist/regras-de-commit.d.ts +3 -0
- package/dist/regras-de-commit.js +15 -0
- package/dist/roadmap.js +3 -3
- package/dist/sessao.d.ts +5 -0
- package/dist/sessao.js +125 -0
- package/dist/skill.d.ts +7 -9
- package/dist/skill.js +101 -30
- package/dist/versao.d.ts +1 -0
- package/dist/versao.js +18 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +21 -10
- package/package.json +3 -3
|
@@ -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
|
|
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
|
|
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. */
|
package/dist/sessao.d.ts
ADDED
|
@@ -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,125 @@
|
|
|
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
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
contexto += "\n\nNão foi possível atualizar as skills; consulte listar_skills quando o serviço voltar.";
|
|
67
|
+
}
|
|
68
|
+
if (r.esperandoIndexacao && inicio) {
|
|
69
|
+
const worker = await subiuOWorker(raiz, r.esperandoIndexacao);
|
|
70
|
+
contexto += `\n\n${r.esperandoIndexacao} memória(s) aguardam indexação; a busca pode estar incompleta. ` +
|
|
71
|
+
(worker.subiu ? `Lote solicitado; isto não comprova conclusão. Consulte status e o log ${worker.log}.` :
|
|
72
|
+
"Worker não iniciado; execute o worker configurado e confira status.");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else if (r.estado === "sem-politica") {
|
|
76
|
+
contexto = "Projeto sem política e/ou briefing. Conduza o briefing com o usuário e, após aprovação, " +
|
|
77
|
+
"grave os dois com escrever_artefato. Defina permissões de commit e push separadamente. " +
|
|
78
|
+
"Depois consulte ler_artefato para verificar e reabra a sessão. Escritas no código permanecem bloqueadas.\n\n" + REGRA_DE_COMMITS;
|
|
79
|
+
}
|
|
80
|
+
else
|
|
81
|
+
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.`;
|
|
82
|
+
await escreveAtomico(arquivo(raiz, c.sessao, host), JSON.stringify({ identidade: id,
|
|
83
|
+
revisao, ciclo, estado: r.estado, em: Date.now() }));
|
|
84
|
+
return saidaDoBoot(host, contexto, r.estado !== "inalcancavel");
|
|
85
|
+
}
|
|
86
|
+
export function saidaDaGuarda(host, motivo, contexto) {
|
|
87
|
+
if (host === "antigravity")
|
|
88
|
+
return { decision: motivo ? "deny" : "ask", reason: motivo ?? contexto ?? "" };
|
|
89
|
+
return { hookSpecificOutput: { hookEventName: "PreToolUse",
|
|
90
|
+
...(motivo ? { permissionDecision: "deny", permissionDecisionReason: motivo } : {}),
|
|
91
|
+
...(contexto ? { additionalContext: contexto } : {}) } };
|
|
92
|
+
}
|
|
93
|
+
// Só ferramentas conhecidas de leitura passam quando o boot falta. Shell não é classificado por regex.
|
|
94
|
+
const LEITURA = /^(Read|Glob|Grep|view_file|list_dir|find_by_name|grep_search|ask_question|AskUserQuestion|request_user_input)$/;
|
|
95
|
+
const LEITURA_MCP = /^mcp__dd[-_]harness__(ler_artefato|ler_roadmap|ler_changelog|listar_skills|ler_skill|ler_memoria|buscar_memoria)$/;
|
|
96
|
+
export async function guardaSessao(host, entrada) {
|
|
97
|
+
const c = normalizaEntrada(host, entrada);
|
|
98
|
+
const raiz = await achaRaiz(c.raiz);
|
|
99
|
+
const e = c.sessao ? await estadoAtual(raiz, c.sessao, host) : null;
|
|
100
|
+
if (e?.estado !== "ok") {
|
|
101
|
+
const onboarding = e?.estado === "sem-politica" && /^mcp__dd[-_]harness__escrever_artefato$/.test(c.ferramenta);
|
|
102
|
+
if (LEITURA.test(c.ferramenta) || LEITURA_MCP.test(c.ferramenta) || onboarding)
|
|
103
|
+
return saidaDaGuarda(host);
|
|
104
|
+
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.");
|
|
105
|
+
}
|
|
106
|
+
const edicoes = edicoesDaChamada(c);
|
|
107
|
+
const avisos = [];
|
|
108
|
+
if (edicoes.length && !await leCache(raiz)) {
|
|
109
|
+
const r = await buscaPolitica(raiz);
|
|
110
|
+
if (r.estado !== "ok") {
|
|
111
|
+
await escreveAtomico(arquivo(raiz, c.sessao, host), JSON.stringify({ estado: "inalcancavel", em: 0 }));
|
|
112
|
+
return saidaDaGuarda(host, "dd-harness: falha ao revalidar contexto após mudança. Reabra a sessão.");
|
|
113
|
+
}
|
|
114
|
+
if (r.memorias)
|
|
115
|
+
await guardaCache(raiz, { memorias: r.memorias });
|
|
116
|
+
}
|
|
117
|
+
for (const edicao of edicoes) {
|
|
118
|
+
const depois = edicao.origem ? await readFile(edicao.origem, "utf8").catch(() => undefined) : edicao.depois;
|
|
119
|
+
const r = JSON.parse(await decideDoHook({ cwd: raiz, session_id: host + c.sessao + e.ciclo,
|
|
120
|
+
tool_input: { file_path: edicao.arquivo, old_string: edicao.antes, new_string: depois } }));
|
|
121
|
+
if (r.hookSpecificOutput.additionalContext)
|
|
122
|
+
avisos.push(r.hookSpecificOutput.additionalContext);
|
|
123
|
+
}
|
|
124
|
+
return saidaDaGuarda(host, undefined, avisos.join("\n\n"));
|
|
125
|
+
}
|
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,15 @@ 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
|
-
*
|
|
44
|
-
*
|
|
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.
|
|
43
|
+
* Atualiza/remove somente ponteiros registrados e intactos. Preserva arquivos manuais,
|
|
44
|
+
* edições locais e recursos adicionais nos diretórios de skills.
|
|
49
45
|
*/
|
|
50
|
-
export declare function escrevePonteirosDeSkills(raiz: string, skills: Skill[]): Promise<{
|
|
46
|
+
export declare function escrevePonteirosDeSkills(raiz: string, skills: Skill[], destinos?: (".claude" | ".agents")[]): Promise<{
|
|
51
47
|
escritos: number;
|
|
48
|
+
removidos: number;
|
|
49
|
+
conflitos: string[];
|
|
52
50
|
}>;
|
|
53
51
|
/** O minimo para criar uma skill: o resto tem default no banco. */
|
|
54
52
|
export type NovaSkillInicial = {
|
package/dist/skill.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
-
|
|
71
|
-
|
|
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,96 @@ usuário em vez de improvisar um procedimento.
|
|
|
87
90
|
`;
|
|
88
91
|
}
|
|
89
92
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
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.
|
|
93
|
+
* Atualiza/remove somente ponteiros registrados e intactos. Preserva arquivos manuais,
|
|
94
|
+
* edições locais e recursos adicionais nos diretórios de skills.
|
|
96
95
|
*/
|
|
97
|
-
export async function escrevePonteirosDeSkills(raiz, skills) {
|
|
98
|
-
let escritos = 0;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
96
|
+
export async function escrevePonteirosDeSkills(raiz, skills, destinos = [".claude"]) {
|
|
97
|
+
let escritos = 0, removidos = 0;
|
|
98
|
+
const conflitos = [];
|
|
99
|
+
const hash = (s) => createHash("sha256").update(s).digest("hex");
|
|
100
|
+
async function temLink(destino, slug) {
|
|
101
|
+
for (const caminho of [join(raiz, destino), join(raiz, destino, "skills"),
|
|
102
|
+
join(raiz, destino, "skills", slug), join(raiz, destino, "skills", slug, "SKILL.md")]) {
|
|
103
|
+
const st = await lstat(caminho).catch((e) => {
|
|
104
|
+
if (e.code === "ENOENT")
|
|
105
|
+
return null;
|
|
106
|
+
throw e;
|
|
107
|
+
});
|
|
108
|
+
if (st?.isSymbolicLink())
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
for (const destino of new Set(destinos)) {
|
|
114
|
+
const manifesto = join(estadoDoRepo(raiz), 'skills-' + destino.slice(1) + '.json');
|
|
115
|
+
let anterior = {};
|
|
116
|
+
try {
|
|
117
|
+
anterior = JSON.parse(await readFile(manifesto, "utf8"));
|
|
118
|
+
}
|
|
119
|
+
catch { /* Sem manifesto, arquivo existente pertence ao dono. */ }
|
|
120
|
+
const proximo = { ...anterior };
|
|
121
|
+
const presentes = new Set();
|
|
122
|
+
for (const skill of skills) {
|
|
123
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(skill.slug))
|
|
124
|
+
throw new Error("slug de skill inválido");
|
|
125
|
+
presentes.add(skill.slug);
|
|
126
|
+
// Hosts sem disable-model-invocation não recebem descoberta automática destas skills.
|
|
127
|
+
// O procedimento continua acessível por listar_skills/ler_skill quando o usuário pedir.
|
|
128
|
+
if (destino === ".agents" && skill.so_por_comando) {
|
|
129
|
+
presentes.delete(skill.slug);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const dir = join(raiz, destino, "skills", skill.slug);
|
|
133
|
+
const caminho = join(dir, "SKILL.md");
|
|
134
|
+
if (await temLink(destino, skill.slug)) {
|
|
135
|
+
conflitos.push(caminho);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const novo = ponteiroDaSkill(skill, destino === ".claude" ? "claude" : "padrao");
|
|
139
|
+
const atual = await readFile(caminho, "utf8").catch((e) => {
|
|
140
|
+
if (e.code === "ENOENT")
|
|
141
|
+
return null;
|
|
142
|
+
throw e;
|
|
143
|
+
});
|
|
144
|
+
if (atual !== null && hash(atual) !== anterior[skill.slug]) {
|
|
145
|
+
conflitos.push(caminho);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (atual !== novo) {
|
|
149
|
+
await mkdir(dir, { recursive: true });
|
|
150
|
+
await escreveAtomico(caminho, novo);
|
|
151
|
+
escritos++;
|
|
152
|
+
}
|
|
153
|
+
proximo[skill.slug] = hash(novo);
|
|
154
|
+
}
|
|
155
|
+
for (const [slug, assinatura] of Object.entries(anterior)) {
|
|
156
|
+
if (presentes.has(slug))
|
|
157
|
+
continue;
|
|
158
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug))
|
|
159
|
+
continue;
|
|
160
|
+
const caminho = join(raiz, destino, "skills", slug, "SKILL.md");
|
|
161
|
+
if (await temLink(destino, slug)) {
|
|
162
|
+
conflitos.push(caminho);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const atual = await readFile(caminho, "utf8").catch((e) => {
|
|
166
|
+
if (e.code === "ENOENT")
|
|
167
|
+
return null;
|
|
168
|
+
throw e;
|
|
169
|
+
});
|
|
170
|
+
if (atual !== null && hash(atual) !== assinatura) {
|
|
171
|
+
conflitos.push(caminho);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (atual !== null) {
|
|
175
|
+
await rm(caminho);
|
|
176
|
+
removidos++;
|
|
177
|
+
}
|
|
178
|
+
delete proximo[slug];
|
|
179
|
+
}
|
|
180
|
+
await escreveAtomico(manifesto, JSON.stringify(proximo));
|
|
110
181
|
}
|
|
111
|
-
return { escritos };
|
|
182
|
+
return { escritos, removidos, conflitos };
|
|
112
183
|
}
|
|
113
184
|
/**
|
|
114
185
|
* Semeia as skills iniciais — so num projeto que ainda nao tem nenhuma.
|
package/dist/versao.d.ts
ADDED
|
@@ -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
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
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dd-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Política, memória e integrações de Claude Code, Codex e Antigravity. CLI sem dependências de runtime.",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"author": "Diego Dias",
|
|
8
8
|
"keywords": [
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"dist"
|
|
35
35
|
],
|
|
36
36
|
"engines": {
|
|
37
|
-
"node": ">=20"
|
|
37
|
+
"node": ">=20.3.0"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"typecheck": "tsc -p . --noEmit",
|