Lição 12 · Curso de Fusão · Mergulhos profundos · Skills profundas (SkillStore)
Alembic × Hermes · O Curso de Fusão · Parte 2 · Mergulhos profundos

Skills profundas — o SkillStore e a disclosure progressiva

Skills são "memória procedural estreita e acionável" — a metade durável do loop de auto-melhoria, complemento da MemoryStore ampla e declarativa. Uma skill é um diretório: um SKILL.md (frontmatter + corpo) mais arquivos opcionais sob references/templates/scripts/assets. A ideia central é a disclosure progressiva: list() lê só o metadado barato; view() carrega o corpo inteiro sob demanda. Um CLONE de skills_tool.py (1.638 LOC) + skill_manager_tool.py (1.233 LOC) do Hermes.

Leia primeiro (fonte primária)
packages/hermes/src/skills/ — skill-store.ts (461 LOC), frontmatter.ts (86), types.ts (124), skill-store.test.ts (488)

Esta lição destila o subsistema de skills do @alembic/hermes — lido verbatim do repositório. Tudo aqui é citado de arquivo real: nada é inventado. A proveniência do CLONE está em docs/hermes-complete-map.md §3.3 (a disclosure progressiva das Anthropic Skills).

Leia a versão simples, ou abra a camada técnica em qualquer seção.
O que você vai conseguir fazer
  • Explicar a disclosure progressiva em três níveis — list() (metadado), view() (corpo), arquivo vinculado — e por que ela mantém o contexto barato.
  • Descrever a anatomia de uma skill como diretório: SKILL.md + os quatro subdirs de suporte, e os dois tetos MAX_NAME_LENGTH = 64 / MAX_DESCRIPTION_LENGTH = 1024.
  • Justificar o parser de frontmatter sem dependências — só pares escalares chave: valor — e por que adicionar yaml é uma stop-condition.
  • Reproduzir a segurança de caminho (validateSupportPath): sem .., não absoluto, sob um subdir allowlistado — transformando "escreva um arquivo" em uma operação limitada.
  • Distinguir o matcher de substring único reusado do memory store do fuzzy match da fonte Python, e dizer por que a fidelidade à convenção do Alembic vence.
Suposições tolas (o que presumimos de você — bem pouco)
  • Você já viu um arquivo Markdown com um cabeçalho --- … --- no topo (o "frontmatter"). Se não viu, a gente mostra um.
  • Você sabe que um diretório guarda arquivos, e que um caminho como references/api.md aponta para um arquivo dentro de uma pasta.
  • Você não precisa saber TypeScript, Zod ou Result. Cada trecho de código vem traduzido em português logo abaixo.
1

A grande ideia


Memória e skills são as duas metades do loop que aprende. A memória é o que o agente sabe (declarativa, ampla); a skill é o que o agente sabe fazer (procedural, estreita). Esta lição é sobre a metade durável.

Pense num cozinheiro. A memória é tudo que ele lembra — fornecedores, preferências do cliente, o que deu errado ontem. A skill é uma receita escrita: passos precisos, repetíveis, que qualquer um na cozinha pode seguir. A receita é durável porque está num papel guardado, não na cabeça de alguém. O SkillStore é o arquivo de receitas: ele guarda, lista, abre, edita e remove cada uma — com travas de segurança para nada se corromper.

Analogia: abrir o índice do livro de receitas (ler só os títulos) é barato. Abrir uma receita inteira custa um pouco mais. Buscar a tabela de conversões anexa custa mais ainda. Você só paga quando precisa. Isso é disclosure progressiva.
Memória (declarativa, ampla) e Skills (procedural, estreita) como as duas metades do loop de auto-melhoria MemoryStore o que o agente SABE declarativa · ampla episódica · semântica · … SkillStore o que o agente SABE FAZER procedural · estreita durável · em disco loop de auto-melhoria
As duas metades complementares. Esta lição cobre a da direita.

O que é, em termos de implementação

Um SkillStore é respaldado por um FsPort injetado (a fronteira de IO do @alembic/etl) sobre um único diretório-base — o análogo do ~/.hermes/skills/ da fonte. Cada skill é um diretório <base>/<name>/ com um SKILL.md mais arquivos de suporte opcionais. A classe é sem estado além do fs/baseDir injetados: toda leitura bate no disco, então stores concorrentes sobre o mesmo diretório permanecem consistentes.

Toda operação falível devolve Result<T, Error> e nunca lança exceção através da fronteira pública — no lugar do dict {"success", "error"} do Python. Quem chama ramifica em result.ok.

Toda operação devolve Result; o chamador ramifica em result.ok (valor) ou !result.ok (erro), nunca try/catch store.patch(…) result.ok? true → use result.value false → trate result.error
Falha é um valor de primeira classe — nenhum try/catch, controle de fluxo uniforme.
CLONE: skills_tool.py (1638 LOC) + skill_manager_tool.py (1233 LOC) destilados no store portável do @alembic/hermes skills_tool.py 1.638 LOC skill_manager_tool.py 1.233 LOC destila packages/hermes/src/skills/ skill-store.ts · frontmatter.ts · types.ts o store portável (não a camada de tool/registry/security)
Não é cópia literal: é o modelo de dados portável destilado, com as deviations conscientes da fonte.
2

Anatomia de uma skill


Uma skill não é um registro num banco: é um diretório no disco. Um arquivo obrigatório (SKILL.md) e quatro pastas de suporte opcionais, e nada mais.

Layout de uma skill: SKILL.md com frontmatter + corpo, e os subdirs references/templates/scripts/assets <base>/minha-skill/ SKILL.md obrigatório frontmatter + corpo references/ templates/ scripts/ assets/ --- (abre a fence) name: minha-skill description: o que ela faz … --- (fecha a fence) … corpo de instruções abaixo … MAX_NAME_LENGTH = 64 · MAX_DESCRIPTION_LENGTH = 1024 os dois tetos carregados verbatim da fonte Um diretório sem um SKILL.md legível e válido é simplesmente IGNORADO — não é uma skill (a tolerância _find_all_skills da fonte).
O SKILL.md é a única peça obrigatória; os quatro subdirs são opcionais e allowlistados.
Guarde istoUma skill é um diretório, e o que a torna uma skill é um SKILL.md com frontmatter válido. Sem isso, o store finge que a pasta nem existe — ela é pulada em silêncio, não vira um erro.

O charset do nome também vem verbatim da fonte (VALID_NAME_RE): /^[a-z0-9][a-z0-9._-]*$/ — letras minúsculas, dígitos, ponto, hífen e sublinhado, começando por letra ou dígito. Tudo validado por Zod e elevado a Result, nunca lançado.

// packages/hermes/src/skills/types.ts:69-80 — o nome filesystem-safe
const SKILL_NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;

export const skillNameSchema = z
  .string()
  .min(1, 'Skill name is required.')
  .max(MAX_NAME_LENGTH, `Skill name exceeds ${MAX_NAME_LENGTH} characters.`)
  .regex(SKILL_NAME_RE, 'Invalid skill name. Use lowercase letters …');

Os subdirs de suporte (SKILL_SUPPORT_DIRS) também são verbatim: references, templates, scripts, assets — espelhando ALLOWED_SUBDIRS da fonte.

3

Os três níveis de custo


Esta é a peça que define o subsistema. Listar tudo é barato; abrir um corpo custa só quando você abre; abrir uma referência custa só quando você precisa. Três níveis, três custos.

Escada de três degraus ascendentes mostrando os três níveis de disclosure progressiva: list lê só o frontmatter, view carrega o corpo inteiro, o arquivo vinculado é o nível mais caro, com o custo de contexto crescendo a cada degrau

Disclosure progressiva: o custo de contexto sobe degrau a degrau — você só paga pelo que abre.

Os três níveis, lado a lado
tier 1 list() lê só frontmatter; tier 2 view(name) carrega o corpo; tier 3 carrega um arquivo vinculado pelo caminho nível 1 · list() lê SÓ o frontmatter name ≤64 · description ≤1024 nunca o corpo nível 2 · view(name) corpo inteiro do SKILL.md + a lista de arquivos vinculados nível 3 · arquivo references/api.md etc. carregado pelo caminho relativo barato listar tudo · pague pelo corpo só ao abrir · pague pela referência só ao precisar
Faça sua aposta antes de ver

Um agente chama list() sobre um diretório com 50 skills. Quanto disco ele lê? Comprometa-se com um palpite — "só o cabeçalho de cada uma" ou "o arquivo inteiro de cada uma" — antes de revelar.

Só o frontmatter de cada skill. O list() chama readMetadata por diretório, que lê o SKILL.md e devolve apenas metadata (name + description) — nunca o corpo. Você paga por um corpo só quando faz view(name) dele, e por um arquivo vinculado só quando o abre. O teste "returns metadata only — name + description, never the body" prova exatamente isso.
// packages/hermes/src/skills/skill-store.ts:80-96 — list (nível 1)
async list(): Promise<Result<readonly SkillMetadata[], Error>> {
  // … ensureDir + readDir(baseDir) …
  for (const entry of entries.value) {
    if (!entry.isDirectory) continue;
    const meta = await this.readMetadata(entry.name);   // só o frontmatter
    if (meta.ok) found.push(meta.value);              // pula dirs não-skill em silêncio
  }
  found.sort((a, b) => a.name.localeCompare(b.name));
  return ok(found);
}

Um diretório sem um SKILL.md legível e de frontmatter válido é simplesmente pulado (a tolerância _find_all_skills da fonte). A lista volta ordenada por nome.

Por que separar metadado de corpo importa tanto

O orçamento de contexto de um agente é finito. Se cada list() carregasse o corpo de toda skill, listar 50 skills jogaria 50 corpos inteiros na janela — desperdício, e possivelmente estouro. Carregar só o frontmatter (name + description) é o mínimo necessário para o agente decidir qual skill abrir. Só então ele paga o nível 2. É a mesma economia do índice de um livro: o índice cabe numa página justamente porque não inclui o texto dos capítulos.

O nível 3 não é um terceiro método

Cuidado com uma confusão sutil: nesta implementação, view(name) devolve o corpo mais a lista de caminhos dos arquivos vinculados (via listLinkedFiles). O "nível 3" é então carregar um desses arquivos pelo seu caminho relativo — uma leitura separada. Não há um view(name, relPath) de dois argumentos aqui; o conceito de níveis descreve quando você paga cada custo, não três assinaturas distintas.

view(name) devolve um Skill: metadata (tier-1) + body (tier-2) + linkedFiles[] (os caminhos para o tier-3) view('minha-skill') Skill { metadata: { name, description } ← nível 1 body: "…instruções inteiras…" ← nível 2 linkedFiles: [ "references/api.md" ] ← caminhos p/ nível 3 }
Um view entrega metadado + corpo + os caminhos; o conteúdo de um arquivo vinculado é uma leitura posterior.
Comparação de custo de contexto: list() de 50 skills (pequeno) versus carregar os 50 corpos (estoura a janela) list() de 50 skills — só metadados contexto pequeno e previsível carregar os 50 corpos — desnecessário desperdício de janela — possivelmente estoura
A economia do índice: o agente decide qual skill abrir pelo metadado, e só então paga o corpo.
4

O parser de frontmatter sem dependências


A fonte parseia frontmatter com YAML completo (CSafeLoader), caindo num chave: valor ingênuo só quando o YAML dá erro. Para ficar sem dependências — adicionar um pacote yaml é uma stop-condition desta run — o clone reproduz exatamente esse fallback e nada mais: só pares escalares de topo.

parseFrontmatter: documento abre com ---, fecha na primeira linha --- (regex), o meio vira pares escalares e o resto vira corpo ---  ←  deve abrir aqui (senão: tudo é corpo, frontmatter vazio) name: minha-skill description: o que a skill faz parseScalarBlock → split no PRIMEIRO ':' linha sem ':' é pulada · chave depois vence ---  ←  fecha na 1ª linha que é ---  (/\n---[ \t]*\n/ desde o offset 3) … todo o resto é o CORPO (newline inicial removido) …
A detecção de fronteira espelha a fonte: abre em ---, fecha na primeira linha ---.
// packages/hermes/src/skills/frontmatter.ts:60-70 — parseScalarBlock
for (const line of block.split('\n')) {
  const colon = line.indexOf(':');
  if (colon === -1) continue;               // sem ':' ⇒ pula a linha
  const key = line.slice(0, colon).trim();
  if (key.length === 0) continue;
  out[key] = line.slice(colon + 1).trim();      // split no 1º ':'; chave depois vence
}

Um documento sem fence de abertura devolve ({}, content) — a coisa toda é o corpo. Mapeamentos aninhados, listas e aspas-com-dois-pontos estão deliberadamente fora de escopo.

Passo a passo: parseando um SKILL.md de verdade
1
O documento começa com ---? Sim. Então o parser corta os 3 primeiros caracteres e procura a próxima fence com /\n---[ \t]*\n/.
2
O bloco entre as fences vira parseScalarBlock: cada linha com : é dividida no primeiro dois-pontos; name e description são extraídos e .trim()-ados.
3
Tudo depois da fence de fechamento é o corpo (com o newline inicial removido). O corpo é .trim()-ado no parseSkill.
4
Agora você: escreva um SKILL.md com description: usa: dois pontos no valor. O que description guarda? — Resposta: usa: dois pontos. O split é só no primeiro :, então o resto da linha (inclusive outros dois-pontos) vira o valor inteiro.
CuidadoEste parser é de propósito limitado. Ele entende exatamente o que importa aqui — name e description escalares. YAML mais rico (metadata aninhado, listas, platforms) fica adiado até um humano aprovar a dependência yaml — adicionar uma é uma stop-condition desta run.
parseScalarBlock divide cada linha no PRIMEIRO dois-pontos: chave à esquerda, todo o resto (mesmo com :) à direita description : usa: dois pontos no valor chave (.trim) valor inteiro — o 2º ':' fica dentro do valor
Split só no primeiro :; uma chave repetida depois vence (semântica de sobrescrita de dict).
5

Segurança de caminho: sem traversal, sob um subdir permitido


Arquivos de suporte são confinados. Um caminho relativo não pode conter segmento .. nem ., não pode ser absoluto, deve usar barras normais, e seu primeiro segmento deve ser um dos quatro subdirs de suporte.

Fluxograma de quatro portões de validação de caminho — barra invertida, caminho absoluto, segmento .. ou ., e subdir permitido — levando a um caminho seguro confinado na skill, com uma tentativa de escape ../../etc bloqueada por um selo

validateSupportPath: quatro travas em sequência transformam "escreva um arquivo" numa operação limitada e auditável.

// packages/hermes/src/skills/skill-store.ts:404-437 — validateSupportPath (condensado)
if (relPath.includes('\\'))   return err(…'use forward slashes');
if (relPath.startsWith('/')) return err(…'must be relative');
const segments = relPath.split('/').filter((s) => s.length > 0);
if (segments.length < 2)     return err(…"must be under references/templates/scripts/assets");
for (const segment of segments)
  if (segment === '..' || segment === '.') return err(…'path traversal is not allowed');
if (!isSupportDir(segments[0]))    return err(…'first segment must be one of …');
Por que tão estrito?

Uma skill pode guardar scripts e assets que o próprio agente autorou; um caminho relativo que escapasse do diretório da skill (../../etc/...) seria um primitivo de escreve-em-qualquer-lugar. Confinar todo caminho de arquivo de suporte sob um subdir allowlistado da skill transforma "escreva um arquivo" numa operação limitada e auditável. Quatro testes guardam isso: um escape com .., um caminho absoluto, um caminho com barra invertida e um arquivo fora dos subdirs permitidos são cada um recusados.

Caminho de entradaResultadoPor quê
references/api.mdokprimeiro segmento allowlistado, dois segmentos, sem traversal
../../secrets.txterrsegmento .. ⇒ "path traversal is not allowed"
/etc/passwderrcomeça com / ⇒ "must be relative"
references\api.mderrbarra invertida ⇒ "use forward slashes"
logs/run.txterrlogs não é um subdir de suporte ⇒ "first segment must be one of …"
6

Um matcher, reusado: patch por substring único


O patch faz um find-and-replace alvo por substring único — e reusa de propósito o mesmo motor fail-closed que o memory store usa (match exato; zero ou múltiplas ocorrências → err), não o matcher fuzzy normalizador-de-espaços da fonte. A escolha é fidelidade à convenção já entregue do Alembic, em vez do helper Python.

replaceUnique: 0 ocorrências → err, exatamente 1 → aplica, 2+ → err ambíguo 0 nenhum match err: "No match …" 1 match único ok: aplica a troca 2+ ambíguo err: "Be more specific"
Exatamente uma ocorrência aplica; qualquer outra contagem falha fechado — nada é escrito.
// packages/hermes/src/skills/skill-store.ts:449-461 — replaceUnique
const first = content.indexOf(find);
if (first === -1) return err(new Error(`No match for '${find}' in SKILL.md.`));
const second = content.indexOf(find, first + find.length);
if (second !== -1) return err(new Error(`Multiple matches for '${find}' …`));
return ok(content.slice(0, first) + replace + content.slice(first + find.length));
Um patch não pode corromper o metadado — re-parseia depois do replace

Depois de substituir, o patch re-parseia o resultado e re-valida o frontmatter; se a edição quebrou a estrutura do SKILL.md em metadado inválido, a escrita é recusada com "Patch would break SKILL.md structure" — a edição nunca chega ao disco. O teste "refuses a patch that would corrupt the frontmatter" prova isso.

Por que reusar o motor da memória? O memory store já tinha um contrato de substring único fail-closed (locateUnique). Reusá-lo em vez de portar o fuzzy_match do Python mantém uma só regra de edição em todo o Alembic: "seja específico o bastante para casar exatamente uma vez". Menos comportamento surpreendente, mais previsibilidade.
patch: replaceUnique troca, depois re-parseia; se o metadado ficou inválido, recusa e não escreve troca aplicada re-parseia: metadado válido? sim → writeFileAtomic (grava) não → "Patch would break SKILL.md"
A edição só chega ao disco se o frontmatter resultante ainda parseia para metadado válido.
7

Delete sem unlink: limpar para vazio


O FsPort não expõe um unlink (nem remoção recursiva). Então delete e removeFile limpam o arquivo para vazio — o que o torna invisível ao list()/view(), porque frontmatter vazio falha a validação. É o marcador "deletado" portável mais próximo possível.

delete escreve string vazia no SKILL.md; frontmatter vazio falha o Zod; a skill some de list e view SKILL.md frontmatter + corpo delete() "" (vazio) writeAtomic(path, '') some de list() e view() frontmatter vazio falha o Zod ⇒ a skill é tratada como inexistente
Sem unlink, "limpar para vazio" é o delete portável — e o teste confirma a invisibilidade.
Quem recusa colisão

create recusa um nome que já existe ("A skill named … already exists.").

Quem recusa ausência

view/edit/patch/delete/removeFile recusam uma skill (ou arquivo) que falta.

Note ainda: edit é uma reescrita completa do corpo que preserva o frontmatter parseado; ele recusa corpo vazio. E todo write é atômico (writeFileAtomic) — uma falha de IO vira err, nunca uma exceção solta.

create falha se a skill JÁ existe; view/edit/patch/delete/removeFile falham se ela NÃO existe create — exige AUSÊNCIA nome livre → cria (ok) já existe → err (colisão) view/edit/patch/delete/removeFile exigem PRESENÇA existe → opera (ok) falta → err (not found)
Duas regras simétricas de existência guardam cada operação — provadas por testes "errors when … already exists" e "errors on a missing skill".
listLinkedFiles varre references/templates/scripts/assets e inclui só arquivos com size maior que zero, ordenados varre os 4 subdirs de suporte → references/api.md size > 0 ✓ inclui scripts/old.sh size = 0 (limpo) ✗ pula linkedFiles[] ordenado só os não-vazios
Arquivos limpos (removidos) têm size 0 e somem da listagem — o mesmo truque do delete.
Operações: list e view leem; create/edit/patch/delete/writeFile/removeFile mutam — todas devolvem Result LEITURAS (disclosure progressiva) list() · n1 view() · n2/3 readMetadata · listLinkedFiles nunca escrevem MUTAÇÕES (write atômico) create edit patch delete writeFile · removeFile todas devolvem Result<…, Error>
Duas leituras (níveis 1 e 2/3) e seis mutações — toda uma devolve Result, nenhuma lança.
8

Recapitulando em 6 slides


A metade durável

Skill = memória procedural

Skills são "memória procedural estreita e acionável" — o que o agente sabe fazer. Complementam a MemoryStore ampla e declarativa.

A forma no disco

Uma skill é um diretório

Um SKILL.md obrigatório (frontmatter + corpo) mais quatro subdirs opcionais: references/templates/scripts/assets. Sem SKILL.md válido, não é uma skill.

A ideia central

Disclosure progressiva

Nível 1 list() lê só o frontmatter. Nível 2 view() carrega o corpo. Nível 3 abre um arquivo vinculado pelo caminho. Você só paga pelo que abre.

Sem dependências

O parser escalar

Frontmatter é parseado por um clone do fallback da fonte: só pares chave: valor de topo, split no primeiro :. Adicionar yaml é stop-condition.

A trava de segurança

Caminho confinado

validateSupportPath: sem ../., não absoluto, sob um subdir allowlistado. "Escrever um arquivo" vira uma operação limitada, nunca um write-anywhere.

Uma só regra de edição

Substring único, fail-closed

patch reusa o motor de substring único do memory store: 0 ou 2+ ocorrências → err; depois re-parseia para não corromper o metadado. Tudo devolve Result.

1 / 6setas

Simples ↔ Técnico: a mesma ideia, duas alturas

Alterne entre a explicação leiga e a precisa. Use a aba "Técnico" quando quiser os nomes reais; a "Simples" quando quiser a intuição.

Em linguagem de gente: o SkillStore é o arquivo de receitas do agente. Você pode folhear só os títulos (barato), abrir uma receita inteira (custa um pouco), ou buscar o anexo de uma receita (custa mais). Você guarda receitas novas, reescreve uma, corrige uma frase específica nela, ou tira uma de circulação. E há porteiros: nenhum anexo pode ser salvo fora da pasta da receita, e uma correção que casa em dois lugares é recusada para você não trocar o pedaço errado.
Com os termos reais: o SkillStore é respaldado por um FsPort injetado sobre um baseDir. list() faz readMetadata (nível 1, só frontmatter); view() faz parseSkill + listLinkedFiles (nível 2/3). As mutações — create/edit/patch/delete/writeFile/removeFile — usam writeFileAtomic e devolvem Result. patch roda replaceUnique (o motor fail-closed do memory store) e re-parseia o resultado; validateSupportPath confina cada arquivo de suporte sob um SKILL_SUPPORT_DIRS allowlistado. Frontmatter via o parser escalar dep-free.

Cartões de memória

Vire cada cartão (clique, ou Enter/Espaço) e tente responder antes de ver o verso. É prática de recuperação — vale mais que reler.

Definição
O que é disclosure progressiva?
clique para virar
Carregar conhecimento em níveis de custo: list() lê só metadado; view() o corpo; o arquivo vinculado depois. Você só paga pelo que abre.
Forma
O que faz de um diretório uma skill?
clique para virar
Um SKILL.md com frontmatter válido (name + description). Sem isso, o diretório é pulado em silêncio — não é uma skill nem um erro.
Tetos
Quais os dois limites verbatim da fonte?
clique para virar
MAX_NAME_LENGTH = 64 e MAX_DESCRIPTION_LENGTH = 1024. Carregados exatamente como a fonte Python os define.
Parser
Por que o frontmatter não usa YAML completo?
clique para virar
Para ficar sem dependências — adicionar yaml é stop-condition. Clona só o fallback escalar da fonte: pares chave: valor de topo.
Segurança
O que validateSupportPath recusa?
clique para virar
Barra invertida, caminho absoluto, qualquer segmento ../., e um primeiro segmento fora de references/templates/scripts/assets.
Edição
Por que patch recusa um substring ambíguo?
clique para virar
Reusa o motor de substring único do memory store: 0 ou 2+ ocorrências → err. Uma só regra de edição em todo o Alembic; nada é escrito se for ambíguo.
As Dez ideias para levar desta lição
  1. Skills são a metade durável e procedural do loop; a memória é a metade ampla e declarativa.
  2. Uma skill é um diretório: SKILL.md obrigatório + references/templates/scripts/assets opcionais.
  3. O que faz de uma pasta uma skill é um SKILL.md de frontmatter válido — caso contrário ela é pulada em silêncio.
  4. Disclosure progressiva em três níveis: list() metadado, view() corpo, arquivo vinculado pelo caminho.
  5. list()só o frontmatter de cada skill — nunca o corpo. Listar 50 skills é barato.
  6. Dois tetos verbatim: MAX_NAME_LENGTH = 64, MAX_DESCRIPTION_LENGTH = 1024.
  7. Frontmatter é parseado por um clone dep-free do fallback escalar; adicionar yaml é stop-condition.
  8. validateSupportPath confina todo arquivo de suporte — sem .., não absoluto, sob um subdir allowlistado.
  9. patch reusa o motor de substring único do memory store (fail-closed) e re-parseia para não corromper o metadado.
  10. Sem unlink no FsPort, delete limpa o arquivo para vazio — frontmatter vazio o some das listagens.
9

Confusões comuns


"Ele parseia frontmatter YAML completo." Não — só pares escalares chave: valor de topo, um clone fiel do caminho de fallback da fonte. YAML mais rico (metadata aninhado, listas) fica adiado até um humano aprovar uma dependência yaml — adicionar uma é uma stop-condition desta run.
"delete remove o diretório." Não — o FsPort não tem unlink recursivo, então delete limpa o SKILL.md para vazio, o que torna a skill invisível ao list()/view() (frontmatter vazio falha a validação). Arquivos de suporte são gerenciados individualmente via removeFile.
"patch usa fuzzy matching como o Hermes." Não — ele reusa de propósito o motor de substring exato do memory store, não o fuzzy_match normalizador-de-espaços da fonte. É fidelidade à convenção já entregue do Alembic sobre o helper Python.
Fora de escopo (de propósito)Vivem na camada de tool/segurança/curator ainda não-conectada: o scan de segurança da escrita-por-agente + rollback (_security_scan_skill), o pin guard (_pinned_guard), descoberta cross-profile, categorias, a semântica absorbed_into do curator, a telemetria de uso (skill_usage.py) e o schema de tool OpenAI + registro no registry. Este é o store portável, não a camada Python de tool/registry/security.
10

Verifique seu entendimento


Três perguntas. Escolha e leia o porquê de cada opção — o feedback ensina tanto quanto a pergunta.

Checagem cumulativa

Acerte as três para fechar a lição. A pontuação aparece abaixo.

1. Um agente chama list() sobre um diretório de 50 skills. O que ele lê do disco?
(b). O list() lê só o frontmatter via readMetadata — nunca o corpo. Você paga por um corpo só ao fazer view(name), e por um arquivo vinculado só ao abri-lo. O teste "returns metadata only — name + description, never the body" confirma. (c) é falso: não há cache; toda leitura bate no disco.
2. writeFile('minha-skill', '../../secrets.txt', …) é chamado. O resultado?
(c). Todo caminho de arquivo de suporte é confinado sob um subdir allowlistado da skill, com recusa explícita de .././absoluto/barra-invertida. Isso transforma "escreva um arquivo" numa operação limitada, nunca um primitivo de escreve-em-qualquer-lugar.
3. patch recebe um substring que aparece duas vezes no SKILL.md. O que acontece?
(d). O replaceUnique exige exatamente uma ocorrência; zero ou múltiplas → err. O skill store reusa de propósito o motor de substring único do Alembic, não o fuzzy matcher Python, pela fidelidade à convenção já entregue. (c) descreve o comportamento da fonte, que NÃO foi portado.
Acertos: 0/3
Em uma frase, para você mesmo: "Uma skill é um ____ com um ____ válido; list() lê só o ____, view() carrega o ____, e patch recusa um substring ____." Se você preenche as cinco lacunas, está pronto para a Lição 13.