dd-harness-mcp 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/src/curar.js +1 -0
- package/dist/cli/src/index.js +29 -0
- package/dist/cli/src/skill.js +143 -0
- package/dist/cli/src/skills-iniciais.js +72 -0
- package/dist/mcp/src/index.js +48 -0
- package/package.json +1 -1
package/dist/cli/src/curar.js
CHANGED
package/dist/cli/src/index.js
CHANGED
|
@@ -21,6 +21,8 @@ import { pergunta, escolha, fechaPerguntas } from "./pergunta.js";
|
|
|
21
21
|
import { buscaPolitica } from "./politica.js";
|
|
22
22
|
import { avisoDeOrdem, blocoDeSessao, criaFase, editaFase, formataChangelog, formataRoadmap, leFases, } from "./roadmap.js";
|
|
23
23
|
import { decideDoHook, guardaCache } from "./cinto.js";
|
|
24
|
+
import { escrevePonteirosDeSkills, leSkills, semeiaSkills } from "./skill.js";
|
|
25
|
+
import { SKILLS_INICIAIS } from "./skills-iniciais.js";
|
|
24
26
|
import { busca } from "./buscar.js";
|
|
25
27
|
import { apaga, arquiva, edita, le, promove } from "./curar.js";
|
|
26
28
|
import { criaPasta } from "./pasta.js";
|
|
@@ -439,6 +441,33 @@ async function comandoStart() {
|
|
|
439
441
|
acrescentado: `atualizado ${arquivo} (apontamento acrescentado ao que já existia)`,
|
|
440
442
|
}[r.estado]);
|
|
441
443
|
}
|
|
444
|
+
// 6.5. Os ponteiros das skills do projeto.
|
|
445
|
+
//
|
|
446
|
+
// O Claude Code descobre skill lendo `.claude/skills/*/SKILL.md` na ABERTURA da sessao,
|
|
447
|
+
// e servidor MCP nao fornece skill — entao o arquivo em disco e obrigatorio. O que ele
|
|
448
|
+
// carrega e so o frontmatter mais a chamada a `ler_skill`: o procedimento fica no
|
|
449
|
+
// servico, e corrigi-lo corrige em todos os repositorios de uma vez.
|
|
450
|
+
//
|
|
451
|
+
// Nunca falha o `start`: projeto sem skill e o caso comum, e nao ter skill nao impede
|
|
452
|
+
// nada do resto.
|
|
453
|
+
try {
|
|
454
|
+
// Projeto novo nasce com as skills iniciais, como nasce com politica e briefing. Num
|
|
455
|
+
// projeto que ja tem as suas, isto nao faz nada.
|
|
456
|
+
const { criadas } = await semeiaSkills(process.cwd(), SKILLS_INICIAIS);
|
|
457
|
+
if (criadas.length > 0) {
|
|
458
|
+
console.log(`criada(s) ${criadas.length} skill(s) inicial(is): ${criadas.join(", ")}`);
|
|
459
|
+
}
|
|
460
|
+
const skills = await leSkills(process.cwd());
|
|
461
|
+
if (skills.length > 0) {
|
|
462
|
+
const { escritos } = await escrevePonteirosDeSkills(process.cwd(), skills);
|
|
463
|
+
console.log(escritos > 0
|
|
464
|
+
? `escritos ${escritos} ponteiro(s) de skill em .claude/skills/`
|
|
465
|
+
: `${skills.length} skill(s) — ponteiros já estavam em dia`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
// Serviço fora do ar ou projeto recém-criado: o `start` segue.
|
|
470
|
+
}
|
|
442
471
|
// 7. Onde esta o monorepo do worker nesta maquina? So pergunta uma vez, e so importa
|
|
443
472
|
// se houver memoria na fila agora — pular aqui nao trava nada, so avisa mais vezes.
|
|
444
473
|
const semWorkerConfigurado = !(await temWorkerConfigurado());
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { cabecalhos, credencial, pede, recusa } from "./api.js";
|
|
4
|
+
// --- rede ---
|
|
5
|
+
export async function leSkills(raiz) {
|
|
6
|
+
const { config, token } = await credencial(raiz);
|
|
7
|
+
const url = new URL(`${config.api}/api/v1/skills`);
|
|
8
|
+
url.searchParams.set("tenant", config.tenant);
|
|
9
|
+
url.searchParams.set("projeto", config.projeto);
|
|
10
|
+
const resposta = await pede(url, { headers: cabecalhos(token) });
|
|
11
|
+
if (!resposta.ok)
|
|
12
|
+
await recusa(resposta);
|
|
13
|
+
return (await resposta.json()).skills;
|
|
14
|
+
}
|
|
15
|
+
export async function leSkill(raiz, slug) {
|
|
16
|
+
const { config, token } = await credencial(raiz);
|
|
17
|
+
const url = new URL(`${config.api}/api/v1/skills/${encodeURIComponent(slug)}`);
|
|
18
|
+
url.searchParams.set("tenant", config.tenant);
|
|
19
|
+
url.searchParams.set("projeto", config.projeto);
|
|
20
|
+
const resposta = await pede(url, { headers: cabecalhos(token) });
|
|
21
|
+
if (!resposta.ok)
|
|
22
|
+
await recusa(resposta);
|
|
23
|
+
return (await resposta.json()).skill;
|
|
24
|
+
}
|
|
25
|
+
export async function criaSkill(raiz,
|
|
26
|
+
// Só o que a API aceita na criação: `atualizada_em` é do servidor, e aceitá-la no tipo
|
|
27
|
+
// prometeria um campo que o Zod descarta em silêncio.
|
|
28
|
+
dados) {
|
|
29
|
+
const { config, token } = await credencial(raiz);
|
|
30
|
+
const resposta = await pede(`${config.api}/api/v1/skills`, {
|
|
31
|
+
method: "POST",
|
|
32
|
+
headers: cabecalhos(token, true),
|
|
33
|
+
body: JSON.stringify({ tenant: config.tenant, projeto: config.projeto, ...dados }),
|
|
34
|
+
});
|
|
35
|
+
if (!resposta.ok)
|
|
36
|
+
await recusa(resposta);
|
|
37
|
+
return (await resposta.json());
|
|
38
|
+
}
|
|
39
|
+
// --- o ponteiro em disco (puro, para ser testado sem rede) ---
|
|
40
|
+
/**
|
|
41
|
+
* Escapa o que quebraria o YAML do frontmatter.
|
|
42
|
+
*
|
|
43
|
+
* A descricao vem de texto livre editado na interface: `: ` no meio dela faz o parser ler
|
|
44
|
+
* uma chave nova, e uma quebra de linha encerra o valor. Aspas duplas com escape resolvem
|
|
45
|
+
* os dois — e um frontmatter quebrado nao da erro visivel, a skill simplesmente some da
|
|
46
|
+
* lista.
|
|
47
|
+
*/
|
|
48
|
+
function comoEscalarYaml(texto) {
|
|
49
|
+
return `"${texto.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\r?\n/g, " ")}"`;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* O SKILL.md que vai para o disco: frontmatter de verdade, corpo que aponta para o MCP.
|
|
53
|
+
*
|
|
54
|
+
* `allowed-tools` sempre inclui a ferramenta que busca o procedimento — sem ela o ponteiro
|
|
55
|
+
* pediria permissao para ler a propria skill, e um "permitir?" antes de cada invocacao
|
|
56
|
+
* ensina a desligar o mecanismo.
|
|
57
|
+
*/
|
|
58
|
+
export function ponteiroDaSkill(skill) {
|
|
59
|
+
const frontmatter = [
|
|
60
|
+
"---",
|
|
61
|
+
`name: ${skill.slug}`,
|
|
62
|
+
`description: ${comoEscalarYaml(skill.descricao)}`,
|
|
63
|
+
];
|
|
64
|
+
if (skill.so_por_comando)
|
|
65
|
+
frontmatter.push("disable-model-invocation: true");
|
|
66
|
+
if (skill.dica_de_argumento) {
|
|
67
|
+
frontmatter.push(`argument-hint: ${comoEscalarYaml(skill.dica_de_argumento)}`);
|
|
68
|
+
}
|
|
69
|
+
const ferramentas = ["mcp__dd-harness__ler_skill", ...skill.ferramentas];
|
|
70
|
+
frontmatter.push(`allowed-tools: ${ferramentas.join(", ")}`);
|
|
71
|
+
if (skill.caminhos.length > 0) {
|
|
72
|
+
frontmatter.push(`paths: ${skill.caminhos.join(", ")}`);
|
|
73
|
+
}
|
|
74
|
+
frontmatter.push("---");
|
|
75
|
+
return `${frontmatter.join("\n")}
|
|
76
|
+
|
|
77
|
+
# ${skill.slug}
|
|
78
|
+
|
|
79
|
+
O procedimento desta skill vive no dd-harness, não neste arquivo.
|
|
80
|
+
|
|
81
|
+
**Chame a ferramenta MCP \`ler_skill\` com \`slug: "${skill.slug}"\` e siga o que ela
|
|
82
|
+
devolver.** É a primeira ação — não tente executar a skill a partir deste arquivo, que é
|
|
83
|
+
só o ponteiro.
|
|
84
|
+
|
|
85
|
+
Se a ferramenta não existir, o servidor MCP do dd-harness não está declarado: avise o
|
|
86
|
+
usuário em vez de improvisar um procedimento.
|
|
87
|
+
`;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Escreve os ponteiros de todas as skills do projeto.
|
|
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.
|
|
96
|
+
*/
|
|
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++;
|
|
110
|
+
}
|
|
111
|
+
return { escritos };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Semeia as skills iniciais — so num projeto que ainda nao tem nenhuma.
|
|
115
|
+
*
|
|
116
|
+
* A checagem e "nenhuma skill", e nao "esta skill especifica": quem apagou
|
|
117
|
+
* `como-desenvolver` tomou uma decisao, e recria-la a cada `start` a desfaria em silencio.
|
|
118
|
+
* Projeto com qualquer skill propria ja passou do ponto de ser semeado.
|
|
119
|
+
*
|
|
120
|
+
* Devolve o que criou. Nunca lanca por falha de rede: semear e conveniencia de projeto
|
|
121
|
+
* novo, e um `start` nao pode falhar porque o mimo nao pode ser entregue.
|
|
122
|
+
*/
|
|
123
|
+
export async function semeiaSkills(raiz, iniciais) {
|
|
124
|
+
try {
|
|
125
|
+
const existentes = await leSkills(raiz);
|
|
126
|
+
if (existentes.length > 0)
|
|
127
|
+
return { criadas: [] };
|
|
128
|
+
const criadas = [];
|
|
129
|
+
for (const nova of iniciais) {
|
|
130
|
+
try {
|
|
131
|
+
await criaSkill(raiz, nova);
|
|
132
|
+
criadas.push(nova.slug);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Uma skill que nao entrou nao impede as outras — nem o `start`.
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { criadas };
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return { criadas: [] };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export const SKILLS_INICIAIS = [
|
|
2
|
+
{
|
|
3
|
+
slug: "como-desenvolver",
|
|
4
|
+
descricao: `Como desenvolver neste projeto — simplicidade, modularização, mudanças cirúrgicas e execução orientada a objetivos. Invoque ao escrever ou editar código para garantir qualidade. Não cobre segurança nem o protocolo de briefing (isso é sempre obrigatório, vive no CLAUDE.md).`,
|
|
5
|
+
conteudo: `# Como Desenvolver
|
|
6
|
+
|
|
7
|
+
Aplique ao escrever ou editar código.
|
|
8
|
+
|
|
9
|
+
## 1. Simplicidade Primeiro
|
|
10
|
+
|
|
11
|
+
**Mínimo de código que resolve o problema. Nada especulativo.**
|
|
12
|
+
|
|
13
|
+
- Sem funcionalidades além do que foi pedido.
|
|
14
|
+
- Sem abstrações para código de uso único.
|
|
15
|
+
- Sem "flexibilidade" ou "configurabilidade" que ninguém pediu.
|
|
16
|
+
- Sem tratamento de erro para cenários impossíveis.
|
|
17
|
+
- Se escreveu 200 linhas e dava em 50, reescreva.
|
|
18
|
+
|
|
19
|
+
O teste: *"Um engenheiro sênior diria que isso está complicado demais?"* Se sim, simplifique.
|
|
20
|
+
|
|
21
|
+
> Simplicidade e modularização (Seção 2) não brigam. Você **divide o que já existe e cresce** — não cria camadas para um futuro hipotético. Extrair um módulo de algo que repete é simplificar; criar um módulo "por via das dúvidas" é a complexidade especulativa que esta seção proíbe.
|
|
22
|
+
|
|
23
|
+
## 2. Modularização e Componentização
|
|
24
|
+
|
|
25
|
+
**Prefira peças pequenas e com responsabilidade única a um monólito grande.** Vale para qualquer código — front-end, back-end, scripts.
|
|
26
|
+
|
|
27
|
+
Um arquivo ou função que faz coisa demais é difícil de ler, testar, reusar e mudar sem quebrar o resto. Quando algo cresce, divida:
|
|
28
|
+
|
|
29
|
+
- **Uma responsabilidade por unidade.** Um arquivo, uma função, um componente deve ter um motivo só para mudar. Se você descreve o que ele faz usando "e" várias vezes, ele faz coisa demais.
|
|
30
|
+
- **Separe as camadas.** Não misture lógica de negócio, acesso a dados e apresentação no mesmo lugar. Cada uma muda por razões diferentes.
|
|
31
|
+
- **Extraia o que repete ou o que cresce** — só depois que existe de fato (ver Seção 1). A regra prática: na **segunda** vez que o mesmo trecho aparece, considere extrair; na terceira, extraia.
|
|
32
|
+
- **No front-end:** quebre telas/páginas grandes em componentes menores e nomeados. Um componente que rola por centenas de linhas quase sempre é vários componentes disfarçados.
|
|
33
|
+
|
|
34
|
+
O teste: *"Eu preciso rolar muito para entender esta unidade, ou guardar várias coisas na cabeça ao mesmo tempo?"* Se sim, divida.
|
|
35
|
+
|
|
36
|
+
**Mas não fragmente à toa.** Dividir em peças minúsculas demais cria o problema oposto — saltar entre dez arquivos para seguir uma linha de raciocínio. Divida quando a unidade carrega mais de uma responsabilidade, não para perseguir uma contagem de linhas.
|
|
37
|
+
|
|
38
|
+
## 3. Mudanças Cirúrgicas
|
|
39
|
+
|
|
40
|
+
**Toque apenas no necessário. Limpe apenas sua própria bagunça.**
|
|
41
|
+
|
|
42
|
+
Ao editar código existente:
|
|
43
|
+
- Não "melhore" código adjacente, comentários ou formatação que não fazem parte da tarefa.
|
|
44
|
+
- Não refatore o que não está quebrado.
|
|
45
|
+
- Mantenha o estilo existente, mesmo que você faria diferente.
|
|
46
|
+
- Viu código morto não relacionado? **Mencione — não delete.**
|
|
47
|
+
|
|
48
|
+
Quando suas mudanças criam órfãos:
|
|
49
|
+
- Remova imports/variáveis/funções que **suas** mudanças tornaram inúteis.
|
|
50
|
+
- Não remova código morto pré-existente sem ser solicitado.
|
|
51
|
+
|
|
52
|
+
O teste: cada linha alterada deve rastrear diretamente à solicitação do usuário.
|
|
53
|
+
|
|
54
|
+
## 4. Execução Orientada a Objetivos
|
|
55
|
+
|
|
56
|
+
**Defina critérios de sucesso. Itere até verificar.**
|
|
57
|
+
|
|
58
|
+
Transforme tarefas vagas em objetivos verificáveis:
|
|
59
|
+
- "Adicionar validação" → "Escreva testes para entradas inválidas, depois faça-os passar."
|
|
60
|
+
- "Corrigir o bug" → "Escreva um teste que o reproduza, depois faça-o passar."
|
|
61
|
+
- "Refatorar X" → "Garanta que os testes passem antes e depois."
|
|
62
|
+
|
|
63
|
+
Para tarefas com múltiplos passos, declare um plano breve:
|
|
64
|
+
\`\`\`
|
|
65
|
+
1. [Passo] → verificar: [checagem]
|
|
66
|
+
2. [Passo] → verificar: [checagem]
|
|
67
|
+
3. [Passo] → verificar: [checagem]
|
|
68
|
+
\`\`\`
|
|
69
|
+
|
|
70
|
+
Critérios fortes deixam você iterar sozinho. Critérios fracos ("faça funcionar") forçam esclarecimentos constantes.`,
|
|
71
|
+
},
|
|
72
|
+
];
|
package/dist/mcp/src/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { criaProjeto } from "../../cli/src/projeto.js";
|
|
|
13
13
|
import { escreveArtefato, leArtefato } from "../../cli/src/artefato.js";
|
|
14
14
|
import { grava } from "../../cli/src/gravar.js";
|
|
15
15
|
import { caminhosDaArvore } from "../../cli/src/diff.js";
|
|
16
|
+
import { leSkill, leSkills } from "../../cli/src/skill.js";
|
|
16
17
|
import { avisoDeOrdem, criaFase, editaFase, formataChangelog, formataRoadmap, leFases, } from "../../cli/src/roadmap.js";
|
|
17
18
|
/**
|
|
18
19
|
* O mesmo servico, outra porta.
|
|
@@ -100,6 +101,53 @@ Os primeiros achados são os que valem: o piso barra tema alheio, mas num Brain
|
|
|
100
101
|
return falha(erro);
|
|
101
102
|
}
|
|
102
103
|
});
|
|
104
|
+
server.registerTool("ler_skill", {
|
|
105
|
+
description: `O procedimento de uma skill deste projeto. É o que o arquivo \`.claude/skills/<slug>/SKILL.md\` manda chamar — ele é só o ponteiro, o conteúdo vive aqui.
|
|
106
|
+
|
|
107
|
+
Chame com o \`slug\` que o ponteiro informou e SIGA o que vier, como se estivesse escrito no próprio arquivo. Não improvise o procedimento a partir do nome da skill.
|
|
108
|
+
|
|
109
|
+
Também serve para consultar uma skill sem invocá-la ("como é mesmo o passo a passo de X?"). \`listar_skills\` mostra o que existe.`,
|
|
110
|
+
inputSchema: z.object({
|
|
111
|
+
slug: z
|
|
112
|
+
.string()
|
|
113
|
+
.min(1)
|
|
114
|
+
.describe("O nome da skill, como aparece no `name` do ponteiro e no comando `/`."),
|
|
115
|
+
}),
|
|
116
|
+
}, async ({ slug }) => {
|
|
117
|
+
try {
|
|
118
|
+
const s = await leSkill(raiz, slug);
|
|
119
|
+
if (!s.conteudo.trim()) {
|
|
120
|
+
return texto(`A skill \`${slug}\` existe mas está sem procedimento escrito. Avise o usuário ` +
|
|
121
|
+
"em vez de improvisar: uma skill vazia é um passo que alguém pretendia definir.");
|
|
122
|
+
}
|
|
123
|
+
return texto(`# ${s.slug}
|
|
124
|
+
|
|
125
|
+
${s.conteudo.trim()}`);
|
|
126
|
+
}
|
|
127
|
+
catch (erro) {
|
|
128
|
+
return falha(erro);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
server.registerTool("listar_skills", {
|
|
132
|
+
description: `As skills deste projeto: nome, quando cada uma serve, e se é invocável só por comando.
|
|
133
|
+
|
|
134
|
+
Devolve a descrição de cada uma, não o procedimento — para o procedimento, \`ler_skill\`. Use quando o usuário perguntar o que existe, ou quando precisar saber se já há skill para um trabalho antes de fazê-lo à mão.`,
|
|
135
|
+
inputSchema: z.object({}),
|
|
136
|
+
}, async () => {
|
|
137
|
+
try {
|
|
138
|
+
const skills = await leSkills(raiz);
|
|
139
|
+
if (skills.length === 0) {
|
|
140
|
+
return texto("Este projeto não tem skill alguma — e isso é válido. Skill é procedimento " +
|
|
141
|
+
"repetido; um que aconteceu uma vez é só uma tarefa.");
|
|
142
|
+
}
|
|
143
|
+
const linhas = skills.map((s) => `- \`${s.slug}\`${s.so_por_comando ? " (só por comando)" : ""}: ${s.descricao}`);
|
|
144
|
+
return texto(`${skills.length} skill(s) neste projeto:\n${linhas.join("\n")}\n\n` +
|
|
145
|
+
"`ler_skill` traz o procedimento de uma delas.");
|
|
146
|
+
}
|
|
147
|
+
catch (erro) {
|
|
148
|
+
return falha(erro);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
103
151
|
server.registerTool("sugerir_ancoras", {
|
|
104
152
|
description: `Os arquivos que esta sessão modificou, para escolher a âncora de uma memória que vai ser gravada.
|
|
105
153
|
|
package/package.json
CHANGED