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 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
- linhas.push(`## ${t.titulo}`, `\`${endereco}\` âncora: \`${t.ancora}\``, "");
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":
@@ -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.25.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",