dd-harness 0.25.0 → 0.27.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/brain.d.ts +5 -0
- package/dist/cinto.d.ts +3 -3
- package/dist/cinto.js +10 -4
- package/dist/curar.d.ts +27 -0
- package/dist/curar.js +50 -0
- package/dist/index.js +88 -1
- package/dist/skill.d.ts +71 -0
- package/dist/skill.js +143 -0
- package/dist/skills-iniciais.d.ts +15 -0
- package/dist/skills-iniciais.js +72 -0
- package/package.json +1 -1
package/dist/brain.d.ts
CHANGED
|
@@ -18,6 +18,11 @@ export type Memoria = {
|
|
|
18
18
|
resumo: string;
|
|
19
19
|
corpo: string;
|
|
20
20
|
status: "ativa" | "historico";
|
|
21
|
+
/**
|
|
22
|
+
* `global` = vale para todo projeto do tenant, inclusive os que ainda nao existem.
|
|
23
|
+
* Ausente em payload antigo, e ai a memoria e de projeto — que era o unico escopo.
|
|
24
|
+
*/
|
|
25
|
+
escopo?: "projeto" | "global";
|
|
21
26
|
dano: string;
|
|
22
27
|
invisibilidade: string;
|
|
23
28
|
externalidade: string;
|
package/dist/cinto.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ export declare function caminhoDoCache(raiz: string): string;
|
|
|
28
28
|
*/
|
|
29
29
|
export type CacheDeAncoras = {
|
|
30
30
|
gravado_em: string;
|
|
31
|
-
memorias: Pick<Memoria, "pasta" | "slug" | "titulo" | "resumo" | "status" | "ancoras">[];
|
|
31
|
+
memorias: Pick<Memoria, "pasta" | "slug" | "titulo" | "resumo" | "status" | "ancoras" | "escopo">[];
|
|
32
32
|
};
|
|
33
33
|
/**
|
|
34
34
|
* Acrescenta UMA memoria ao cache, sem esperar a proxima sessao.
|
|
@@ -44,7 +44,7 @@ export type CacheDeAncoras = {
|
|
|
44
44
|
*
|
|
45
45
|
* Idempotente pelo endereco: regravar a mesma memoria substitui a entrada, nunca duplica.
|
|
46
46
|
*/
|
|
47
|
-
export declare function acrescentaAoCache(raiz: string, memoria: Pick<Memoria, "pasta" | "slug" | "titulo" | "resumo" | "status" | "ancoras">): Promise<void>;
|
|
47
|
+
export declare function acrescentaAoCache(raiz: string, memoria: Pick<Memoria, "pasta" | "slug" | "titulo" | "resumo" | "status" | "ancoras" | "escopo">): Promise<void>;
|
|
48
48
|
/** Guarda as ancoras do Brain para o hook consultar sem rede. Falha em silencio: cache e otimizacao, nao contrato. */
|
|
49
49
|
export declare function guardaCache(raiz: string, brain: Brain): Promise<void>;
|
|
50
50
|
export declare function leCache(raiz: string): Promise<CacheDeAncoras | null>;
|
|
@@ -78,7 +78,7 @@ export declare function alerta(tocadas: {
|
|
|
78
78
|
pasta: string;
|
|
79
79
|
memoria: string;
|
|
80
80
|
ancora: string;
|
|
81
|
-
}[], resumos: Map<string, string>): string;
|
|
81
|
+
}[], resumos: Map<string, string>, globais?: Set<string>): string;
|
|
82
82
|
/**
|
|
83
83
|
* O hook inteiro: le a entrada, cruza, devolve o JSON que o Claude Code espera.
|
|
84
84
|
*
|
package/dist/cinto.js
CHANGED
|
@@ -66,13 +66,14 @@ export async function guardaCache(raiz, brain) {
|
|
|
66
66
|
gravado_em: new Date().toISOString(),
|
|
67
67
|
memorias: brain.memorias
|
|
68
68
|
.filter((m) => m.status === "ativa" && m.ancoras.length > 0)
|
|
69
|
-
.map(({ pasta, slug, titulo, resumo, status, ancoras }) => ({
|
|
69
|
+
.map(({ pasta, slug, titulo, resumo, status, ancoras, escopo }) => ({
|
|
70
70
|
pasta,
|
|
71
71
|
slug,
|
|
72
72
|
titulo,
|
|
73
73
|
resumo,
|
|
74
74
|
status,
|
|
75
75
|
ancoras,
|
|
76
|
+
escopo,
|
|
76
77
|
})),
|
|
77
78
|
};
|
|
78
79
|
try {
|
|
@@ -143,7 +144,7 @@ export function caminhoRelativo(raiz, arquivo) {
|
|
|
143
144
|
* Vazio e o caso comum e tem que sair barato: a maioria das edicoes nao toca ancora
|
|
144
145
|
* alguma, e o silencio ai nao e falta de aviso, e a ausencia de motivo para avisar.
|
|
145
146
|
*/
|
|
146
|
-
export function alerta(tocadas, resumos) {
|
|
147
|
+
export function alerta(tocadas, resumos, globais = new Set()) {
|
|
147
148
|
if (tocadas.length === 0)
|
|
148
149
|
return "";
|
|
149
150
|
const linhas = [
|
|
@@ -152,7 +153,11 @@ export function alerta(tocadas, resumos) {
|
|
|
152
153
|
];
|
|
153
154
|
for (const t of tocadas) {
|
|
154
155
|
const endereco = `${t.pasta}/${t.memoria}`;
|
|
155
|
-
|
|
156
|
+
// Marcar a global muda o peso do que se le: a licao nao fala DESTE projeto, fala de
|
|
157
|
+
// todos. Sem a marca, o agente a avalia como decisao local e pode concluir que "aqui
|
|
158
|
+
// e diferente" — que e exatamente o raciocinio que a promocao existiu para vencer.
|
|
159
|
+
const marca = globais.has(endereco) ? " · MEMÓRIA GLOBAL" : "";
|
|
160
|
+
linhas.push(`## ${t.titulo}${marca}`, `\`${endereco}\` — âncora: \`${t.ancora}\``, "");
|
|
156
161
|
const resumo = resumos.get(endereco);
|
|
157
162
|
if (resumo)
|
|
158
163
|
linhas.push(resumo, "");
|
|
@@ -203,11 +208,12 @@ export async function decideDoHook(entrada) {
|
|
|
203
208
|
if (novas.length === 0)
|
|
204
209
|
return permitir;
|
|
205
210
|
const resumos = new Map(cache.memorias.map((m) => [`${m.pasta}/${m.slug}`, m.resumo]));
|
|
211
|
+
const globais = new Set(cache.memorias.filter((m) => m.escopo === "global").map((m) => `${m.pasta}/${m.slug}`));
|
|
206
212
|
return JSON.stringify({
|
|
207
213
|
hookSpecificOutput: {
|
|
208
214
|
hookEventName: "PreToolUse",
|
|
209
215
|
permissionDecision: "allow",
|
|
210
|
-
additionalContext: alerta(novas, resumos),
|
|
216
|
+
additionalContext: alerta(novas, resumos, globais),
|
|
211
217
|
},
|
|
212
218
|
});
|
|
213
219
|
}
|
package/dist/curar.d.ts
CHANGED
|
@@ -54,3 +54,30 @@ export declare function arquiva(raiz: string, endereco: string, opcoes: Arquivam
|
|
|
54
54
|
endereco: string;
|
|
55
55
|
motivo: string;
|
|
56
56
|
}>;
|
|
57
|
+
/**
|
|
58
|
+
* Promove a memoria a global — ou a traz de volta ao projeto de origem.
|
|
59
|
+
*
|
|
60
|
+
* Global vale para TODO projeto do espaco, inclusive os que ainda nao existem. E o degrau
|
|
61
|
+
* acima de `memory_projects`, que lista projetos nomeados: aqui o alcance deixa de ser
|
|
62
|
+
* uma lista e vira uma propriedade.
|
|
63
|
+
*/
|
|
64
|
+
export declare function promove(raiz: string, endereco: string, global: boolean): Promise<{
|
|
65
|
+
escopo: string;
|
|
66
|
+
global: boolean;
|
|
67
|
+
tenant: string | null;
|
|
68
|
+
}>;
|
|
69
|
+
/**
|
|
70
|
+
* Apaga de verdade, em cascata — ancoras, deriva medida, vinculos, tudo.
|
|
71
|
+
*
|
|
72
|
+
* Diferente de `arquiva`, que e o caminho normal: arquivar guarda o conteudo porque o que
|
|
73
|
+
* a memoria dizia pode voltar a importar. Isto e para o que nunca deveria ter existido.
|
|
74
|
+
*
|
|
75
|
+
* Sem `confirmacao`, o servidor RECUSA e devolve o que a cascata levaria junto — e so
|
|
76
|
+
* entao se repete a chamada com o nome do espaco. Duas etapas de proposito: DELETE nao
|
|
77
|
+
* tem desfazer, e a cascata e invisivel de fora.
|
|
78
|
+
*/
|
|
79
|
+
export declare function apaga(raiz: string, endereco: string, confirmacao?: string): Promise<{
|
|
80
|
+
apagou: boolean;
|
|
81
|
+
detalhe: string;
|
|
82
|
+
confirmacaoEsperada?: string;
|
|
83
|
+
}>;
|
package/dist/curar.js
CHANGED
|
@@ -92,3 +92,53 @@ export async function arquiva(raiz, endereco, opcoes) {
|
|
|
92
92
|
const lido = (await resposta.json());
|
|
93
93
|
return lido;
|
|
94
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Promove a memoria a global — ou a traz de volta ao projeto de origem.
|
|
97
|
+
*
|
|
98
|
+
* Global vale para TODO projeto do espaco, inclusive os que ainda nao existem. E o degrau
|
|
99
|
+
* acima de `memory_projects`, que lista projetos nomeados: aqui o alcance deixa de ser
|
|
100
|
+
* uma lista e vira uma propriedade.
|
|
101
|
+
*/
|
|
102
|
+
export async function promove(raiz, endereco, global) {
|
|
103
|
+
const { config, token } = await credencial(raiz);
|
|
104
|
+
const resposta = await pede(`${config.api}/api/v1/memorias/${endereco}/escopo`, {
|
|
105
|
+
method: "PUT",
|
|
106
|
+
headers: cabecalhos(token, true),
|
|
107
|
+
body: JSON.stringify({ tenant: config.tenant, projeto: config.projeto, global }),
|
|
108
|
+
});
|
|
109
|
+
if (!resposta.ok)
|
|
110
|
+
await recusa(resposta);
|
|
111
|
+
return (await resposta.json());
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Apaga de verdade, em cascata — ancoras, deriva medida, vinculos, tudo.
|
|
115
|
+
*
|
|
116
|
+
* Diferente de `arquiva`, que e o caminho normal: arquivar guarda o conteudo porque o que
|
|
117
|
+
* a memoria dizia pode voltar a importar. Isto e para o que nunca deveria ter existido.
|
|
118
|
+
*
|
|
119
|
+
* Sem `confirmacao`, o servidor RECUSA e devolve o que a cascata levaria junto — e so
|
|
120
|
+
* entao se repete a chamada com o nome do espaco. Duas etapas de proposito: DELETE nao
|
|
121
|
+
* tem desfazer, e a cascata e invisivel de fora.
|
|
122
|
+
*/
|
|
123
|
+
export async function apaga(raiz, endereco, confirmacao) {
|
|
124
|
+
const { config, token } = await credencial(raiz);
|
|
125
|
+
const resposta = await pede(`${config.api}/api/v1/memorias/${endereco}/apagar`, {
|
|
126
|
+
method: "POST",
|
|
127
|
+
headers: cabecalhos(token, true),
|
|
128
|
+
body: JSON.stringify({
|
|
129
|
+
tenant: config.tenant,
|
|
130
|
+
projeto: config.projeto,
|
|
131
|
+
...(confirmacao ? { confirmacao } : {}),
|
|
132
|
+
}),
|
|
133
|
+
});
|
|
134
|
+
// 409 aqui nao e falha: e "ainda nao" — falta confirmar, ou ha uma arquivada apontando
|
|
135
|
+
// para esta. O texto do servidor e a resposta, entao ele passa adiante em vez de virar
|
|
136
|
+
// excecao com mensagem generica.
|
|
137
|
+
if (resposta.status === 409) {
|
|
138
|
+
const lido = (await resposta.json());
|
|
139
|
+
return { apagou: false, detalhe: lido.erro, confirmacaoEsperada: lido.confirmacao_esperada };
|
|
140
|
+
}
|
|
141
|
+
if (!resposta.ok)
|
|
142
|
+
await recusa(resposta);
|
|
143
|
+
return (await resposta.json());
|
|
144
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -21,8 +21,10 @@ 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
|
-
import { arquiva, edita, le } from "./curar.js";
|
|
27
|
+
import { apaga, arquiva, edita, le, promove } from "./curar.js";
|
|
26
28
|
import { criaPasta } from "./pasta.js";
|
|
27
29
|
import { criaProjeto, listaTenants } from "./projeto.js";
|
|
28
30
|
import { reancora } from "./reancorar.js";
|
|
@@ -52,6 +54,15 @@ const AJUDA = `dd-harness — a política e o Brain do projeto, no serviço
|
|
|
52
54
|
dd-harness arquivar <pasta>/<slug> --motivo <obsoleta|incorreta|fora_dos_filtros>
|
|
53
55
|
[--substituida-por <pasta>/<slug>]
|
|
54
56
|
tira de circulação sem apagar
|
|
57
|
+
dd-harness promover <pasta>/<slug>
|
|
58
|
+
torna a memória global: vale para TODO
|
|
59
|
+
projeto do espaço, inclusive os futuros
|
|
60
|
+
dd-harness despromover <pasta>/<slug>
|
|
61
|
+
traz de volta ao alcance dos vínculos
|
|
62
|
+
dd-harness apagar <pasta>/<slug> [--confirmar <espaço>]
|
|
63
|
+
apaga de vez, em cascata — sem desfazer.
|
|
64
|
+
Para tirar de circulação guardando o
|
|
65
|
+
conteúdo, use "arquivar"
|
|
55
66
|
dd-harness buscar "<pergunta>" acha memória por relevância, não por arquivo
|
|
56
67
|
dd-harness ler <pasta>/<slug> imprime a memória inteira, no formato de gravar
|
|
57
68
|
dd-harness reancorar <pasta>/<slug> --de "<alvo>" --para "<alvo>"
|
|
@@ -430,6 +441,33 @@ async function comandoStart() {
|
|
|
430
441
|
acrescentado: `atualizado ${arquivo} (apontamento acrescentado ao que já existia)`,
|
|
431
442
|
}[r.estado]);
|
|
432
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
|
+
}
|
|
433
471
|
// 7. Onde esta o monorepo do worker nesta maquina? So pergunta uma vez, e so importa
|
|
434
472
|
// se houver memoria na fila agora — pular aqui nao trava nada, so avisa mais vezes.
|
|
435
473
|
const semWorkerConfigurado = !(await temWorkerConfigurado());
|
|
@@ -520,6 +558,49 @@ async function comandoEditar(argv) {
|
|
|
520
558
|
console.log(` ${r.ancoras} âncora(s).`);
|
|
521
559
|
}
|
|
522
560
|
const MOTIVOS = ["obsoleta", "incorreta", "fora_dos_filtros"];
|
|
561
|
+
/**
|
|
562
|
+
* `promover` / `despromover` — o alcance da memoria.
|
|
563
|
+
*
|
|
564
|
+
* Global vale para TODO projeto do espaco, inclusive os que ainda nao existem. Nao e o
|
|
565
|
+
* mesmo que `projetos:` no frontmatter, que lista projetos nomeados: la o alcance e uma
|
|
566
|
+
* lista, aqui e uma propriedade.
|
|
567
|
+
*/
|
|
568
|
+
async function comandoPromover(argv, global) {
|
|
569
|
+
const endereco = argv[0];
|
|
570
|
+
const verbo = global ? "promover" : "despromover";
|
|
571
|
+
if (!endereco || endereco.startsWith("-") || !endereco.includes("/")) {
|
|
572
|
+
throw new Error(`uso: dd-harness ${verbo} <pasta>/<slug>`);
|
|
573
|
+
}
|
|
574
|
+
const r = await promove(process.cwd(), endereco, global);
|
|
575
|
+
console.log(global
|
|
576
|
+
? `${endereco} agora é GLOBAL — vale para todo projeto de ${r.tenant}, inclusive os que ainda não existem.`
|
|
577
|
+
: `${endereco} voltou a valer só para os projetos a que está vinculada.`);
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* `apagar` — o unico caminho que destroi.
|
|
581
|
+
*
|
|
582
|
+
* Duas etapas, e a segunda pede o nome do espaco digitado. Nao e cerimonia: a cascata leva
|
|
583
|
+
* ancoras e toda a deriva medida delas, e nao ha desfazer. Arquivar continua sendo o
|
|
584
|
+
* caminho normal — isto e para o que nunca deveria ter existido.
|
|
585
|
+
*/
|
|
586
|
+
async function comandoApagar(argv) {
|
|
587
|
+
const endereco = argv[0];
|
|
588
|
+
if (!endereco || endereco.startsWith("-") || !endereco.includes("/")) {
|
|
589
|
+
throw new Error("uso: dd-harness apagar <pasta>/<slug> [--confirmar <espaço>]");
|
|
590
|
+
}
|
|
591
|
+
const r = await apaga(process.cwd(), endereco, argumento(argv, "confirmar"));
|
|
592
|
+
if (!r.apagou) {
|
|
593
|
+
console.log(r.detalhe);
|
|
594
|
+
if (r.confirmacaoEsperada) {
|
|
595
|
+
console.log(`
|
|
596
|
+
dd-harness apagar ${endereco} --confirmar ${r.confirmacaoEsperada}`);
|
|
597
|
+
}
|
|
598
|
+
// Sem exit diferente de zero: recusar por falta de confirmacao nao e falha, e o
|
|
599
|
+
// caminho normal da primeira chamada.
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
console.log(`${endereco} apagada — ${r.detalhe}`);
|
|
603
|
+
}
|
|
523
604
|
async function comandoArquivar(argv) {
|
|
524
605
|
const endereco = argv[0];
|
|
525
606
|
const motivo = argumento(argv, "motivo");
|
|
@@ -995,6 +1076,12 @@ async function principal() {
|
|
|
995
1076
|
return comandoEditar(resto);
|
|
996
1077
|
case "arquivar":
|
|
997
1078
|
return comandoArquivar(resto);
|
|
1079
|
+
case "promover":
|
|
1080
|
+
return comandoPromover(resto, true);
|
|
1081
|
+
case "despromover":
|
|
1082
|
+
return comandoPromover(resto, false);
|
|
1083
|
+
case "apagar":
|
|
1084
|
+
return comandoApagar(resto);
|
|
998
1085
|
case "ler":
|
|
999
1086
|
return comandoLer(resto);
|
|
1000
1087
|
case "buscar":
|
package/dist/skill.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills: o procedimento mora no servico, o disco guarda so o ponteiro.
|
|
3
|
+
*
|
|
4
|
+
* O Claude Code descobre skill lendo `.claude/skills/<nome>/SKILL.md` na ABERTURA da
|
|
5
|
+
* sessao, e nenhum servidor MCP pode fornecer skill — o protocolo serve ferramentas e
|
|
6
|
+
* prompts, nao skills. Entao o arquivo em disco e inevitavel.
|
|
7
|
+
*
|
|
8
|
+
* O que e evitavel e o CONTEUDO em disco. O ponteiro carrega o frontmatter real (que e o
|
|
9
|
+
* que o host le para listar e decidir invocar) e um corpo de poucas linhas mandando
|
|
10
|
+
* chamar `ler_skill`. O procedimento fica num lugar so, e corrigi-lo corrige em todos os
|
|
11
|
+
* repositorios — que e o motivo de skills terem saido do disco.
|
|
12
|
+
*
|
|
13
|
+
* Repare que o custo de contexto NAO e o motivo: o Claude Code ja carrega so a descricao
|
|
14
|
+
* de cada skill e busca o corpo quando invocada. O ponteiro nao economiza nada ali — ele
|
|
15
|
+
* resolve divergencia, nao peso.
|
|
16
|
+
*/
|
|
17
|
+
export type Skill = {
|
|
18
|
+
slug: string;
|
|
19
|
+
descricao: string;
|
|
20
|
+
so_por_comando: boolean;
|
|
21
|
+
ferramentas: string[];
|
|
22
|
+
dica_de_argumento: string | null;
|
|
23
|
+
caminhos: string[];
|
|
24
|
+
atualizada_em: string;
|
|
25
|
+
};
|
|
26
|
+
export type SkillCompleta = Skill & {
|
|
27
|
+
conteudo: string;
|
|
28
|
+
};
|
|
29
|
+
export declare function leSkills(raiz: string): Promise<Skill[]>;
|
|
30
|
+
export declare function leSkill(raiz: string, slug: string): Promise<SkillCompleta>;
|
|
31
|
+
export declare function criaSkill(raiz: string, dados: NovaSkillInicial & Partial<Pick<Skill, "so_por_comando" | "ferramentas" | "caminhos" | "dica_de_argumento">>): Promise<{
|
|
32
|
+
skill: Skill;
|
|
33
|
+
}>;
|
|
34
|
+
/**
|
|
35
|
+
* O SKILL.md que vai para o disco: frontmatter de verdade, corpo que aponta para o MCP.
|
|
36
|
+
*
|
|
37
|
+
* `allowed-tools` sempre inclui a ferramenta que busca o procedimento — sem ela o ponteiro
|
|
38
|
+
* pediria permissao para ler a propria skill, e um "permitir?" antes de cada invocacao
|
|
39
|
+
* ensina a desligar o mecanismo.
|
|
40
|
+
*/
|
|
41
|
+
export declare function ponteiroDaSkill(skill: Skill): string;
|
|
42
|
+
/**
|
|
43
|
+
* Escreve os ponteiros de todas as skills do projeto.
|
|
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.
|
|
49
|
+
*/
|
|
50
|
+
export declare function escrevePonteirosDeSkills(raiz: string, skills: Skill[]): Promise<{
|
|
51
|
+
escritos: number;
|
|
52
|
+
}>;
|
|
53
|
+
/** O minimo para criar uma skill: o resto tem default no banco. */
|
|
54
|
+
export type NovaSkillInicial = {
|
|
55
|
+
slug: string;
|
|
56
|
+
descricao: string;
|
|
57
|
+
conteudo: string;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Semeia as skills iniciais — so num projeto que ainda nao tem nenhuma.
|
|
61
|
+
*
|
|
62
|
+
* A checagem e "nenhuma skill", e nao "esta skill especifica": quem apagou
|
|
63
|
+
* `como-desenvolver` tomou uma decisao, e recria-la a cada `start` a desfaria em silencio.
|
|
64
|
+
* Projeto com qualquer skill propria ja passou do ponto de ser semeado.
|
|
65
|
+
*
|
|
66
|
+
* Devolve o que criou. Nunca lanca por falha de rede: semear e conveniencia de projeto
|
|
67
|
+
* novo, e um `start` nao pode falhar porque o mimo nao pode ser entregue.
|
|
68
|
+
*/
|
|
69
|
+
export declare function semeiaSkills(raiz: string, iniciais: NovaSkillInicial[]): Promise<{
|
|
70
|
+
criadas: string[];
|
|
71
|
+
}>;
|
package/dist/skill.js
ADDED
|
@@ -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,15 @@
|
|
|
1
|
+
import type { NovaSkillInicial } from "./skill.js";
|
|
2
|
+
/**
|
|
3
|
+
* As skills que todo projeto novo nasce tendo.
|
|
4
|
+
*
|
|
5
|
+
* Mesma ideia da politica e do briefing: projeto recem-criado ja vem com o minimo para
|
|
6
|
+
* trabalhar, em vez de comecar vazio esperando que alguem lembre de escrever. A diferenca
|
|
7
|
+
* e que skill e EDITAVEL na interface — o que entra aqui e ponto de partida do projeto,
|
|
8
|
+
* nao regra do produto, e ajusta-la e o uso esperado.
|
|
9
|
+
*
|
|
10
|
+
* Semeadas so quando o projeto NAO TEM skill alguma. Um projeto que ja tem as suas nao
|
|
11
|
+
* recebe nada: se alguem apagou `como-desenvolver`, foi decisao — e recria-la a cada
|
|
12
|
+
* `start` desfaria a decisao em silencio, toda vez.
|
|
13
|
+
*/
|
|
14
|
+
export type SkillInicial = NovaSkillInicial;
|
|
15
|
+
export declare const SKILLS_INICIAIS: SkillInicial[];
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dd-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"type": "module",
|
|
5
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
6
|
"license": "UNLICENSED",
|