Ontem, escrevi uma mensagem de commit com o Claude Code. O diff era uma alteração de uma linha: um erro de digitação em um comentário. Claude Opus leu o diff, pensou por dois segundos e gerou fix: correct typo in auth comment. Para isso, consumiu cerca de 800 tokens de entrada e 30 de saída, a $15 e $75 por milhão, respectivamente. Custo: uma fração de centavo. Mas multiplique isso por 40 commits por dia, 250 dias por ano, em uma empresa com 200 desenvolvedores usando agentes de codificação, e a fração de centavo se transforma em milhares de dólares gastos em uma tarefa intelectual equivalente a colocar band-aids.
O problema não é que o Opus seja caro. O problema é que os agentes de codificação não diferenciam entre tarefas de $0.001 e tarefas de $0.10. Tudo passa pelo mesmo modelo. Gerar uma mensagem de commit, classificar uma issue, validar um formato – tudo vai para o modelo grande ao mesmo custo de projetar uma arquitetura de microsserviços. É o equivalente a contratar um cirurgião para colocar band-aids.
Os números
Vamos fazer as contas com os preços do Claude Opus 4 (a geração anterior, a que a maioria ainda usa em produção):
| Tarefa | Tokens entrada | Tokens saída | Custo |
|---|---|---|---|
| Mensagem de commit (diff pequeno) | ~800 | ~30 | $0.014 |
| Classificar uma issue | ~500 | ~50 | $0.011 |
| Validar formato de commit | ~300 | ~20 | $0.006 |
| Resumo de stand-up | ~2000 | ~200 | $0.045 |
Nenhuma dessas tarefas precisa de um modelo com 2 trilhões de parâmetros e capacidade de raciocínio multi-passo. São tarefas de classificação e geração com restrições rigorosas. O equivalente a ordenar cartas por cor.
Com o modelo on-device da Apple Intelligence (3B parâmetros, incluído no macOS 26): custo $0.00, latência ~300ms, sem rede, sem chave API.
foundation-hooks
foundation-hooks é um conjunto de 4 binários Swift que utilizam o framework Foundation Models da Apple para automatizar tarefas de desenvolvimento que não justificam um modelo cloud:
| Binário | Função | Hook git |
|---|---|---|
fm-commit-msg | Gera mensagens de commit convencionais a partir do diff | prepare-commit-msg |
fm-validate-msg | Valida formato e sugere correções | commit-msg |
fm-lql-create | Classifica e cria issues no Linear via lql | CLI |
fm-lql-standup | Gera resumo de stand-up a partir de git log + issues | CLI |
Os quatro compartilham o mesmo padrão: definir uma struct Swift com @Generable, alimentar o modelo com contexto mínimo e obter saída estruturada em milissegundos.
Instalação:
git clone https://github.com/frr149/foundation-hooks
cd foundation-hooks
make build && make install-hooks REPO=/path/to/your/repo
A partir desse momento, cada git commit gera automaticamente uma mensagem convencional. O hook está instalado em 11 repositórios de produção há duas semanas.
Como funciona: @Generable e constrained decoding
Esta é a parte que merece atenção técnica. @Generable não é “pedir ao modelo que devolva JSON e torcer para que funcione”. Trata-se de constrained decoding – o modelo literalmente não pode gerar tokens que violem o esquema.
O mecanismo
@Generableé uma macro Swift que gera um JSON Schema em tempo de compilação a partir da struct.- O framework injeta esse esquema no prompt como especificação de formato de resposta.
- Durante a inferência, em cada passo de decodificação, aplica-se o token masking: os tokens do vocabulário que produziriam uma saída inválida conforme o esquema são mascarados (probabilidade 0 no softmax).
- O modelo só pode escolher entre tokens válidos.
A Apple descreve isso como “guided generation” na documentação da WWDC25. É a mesma técnica que a OpenAI utiliza com response_format: json_schema e que a Anthropic aplica em uso de ferramentas. A diferença: a Apple integra isso no sistema de tipos do Swift. Você define a struct, o compilador gera o esquema, o runtime o aplica na inferência. Segurança de tipos de ponta a ponta.
Os três níveis de restrição
@Generable
struct CommitMessage {
// Nível 1: restrição RÍGIDA — enum efetivo
// Token masking ativo: apenas "fix", "feat", "refactor", etc.
// Os tokens que formariam "bug" ou "update" têm probabilidade 0.
@Guide(.anyOf(["fix", "feat", "refactor", "test", "docs", "chore", "style"]))
var type: String
// Nível 2: restrição SUAVE — como um system prompt para este campo
// O modelo tende a seguir, mas não é forçado.
@Guide(description: "Escopo da alteração, por exemplo, auth, ui, db. Uma palavra, minúscula.")
var scope: String
// Nível 3: sem restrição — string livre, o modelo decide
var subject: String
}
A analogia: anyOf é um dropdown, description é um input com placeholder, e um campo sem Guide é um textarea vazio. A diferença entre os três não é de grau, mas de mecanismo. O primeiro opera a nível de tokens (o modelo não pode escapar), o segundo opera a nível de prompt (o modelo tende a seguir), o terceiro não tem guia.
Isso é relevante porque o caso de uso dos hooks é exatamente o cenário onde a restrição rígida brilha. O commit type precisa ser um dos 7 valores. Não há ambiguidade, criatividade ou raciocínio. É pura classificação. Um modelo de 3B parâmetros com constrained decoding faz isso tão bem quanto um de 200B. A diferença é que um leva 300ms e é gratuito, e o outro leva 2 segundos e custa dinheiro.
O código completo de um hook
Este é fm-commit-msg, o hook de prepare-commit-msg. São 106 linhas de Swift, sem dependências externas:
import Foundation
import FoundationModels
@Generable
struct CommitMessage {
@Guide(description: "Type of change")
@Guide(.anyOf(["fix", "feat", "refactor", "test", "docs", "chore", "style"]))
var type: String
@Guide(description: "Scope of the change, e.g. auth, ui, db, api. One word, lowercase.")
var scope: String
@Guide(description: "Imperative summary of the change, max 50 chars, lowercase, no period")
var subject: String
}
guard SystemLanguageModel.default.isAvailable else {
exit(0) // Sem Apple Intelligence — sair silenciosamente, usuário escreve por conta própria
}
Três destaques:
Degradação graciosa: se a Apple Intelligence não está disponível (Mac sem Apple Silicon, modelo não baixado), o hook sai com código 0 e o git continua normalmente. Nunca bloqueia.
Não inventa: o modelo recebe
git diff --cached --state um patch truncado a 3000 caracteres. É suficiente para classificar e resumir, mas insuficiente para confabular.Não substitui o humano: a mensagem é escrita no arquivo de commit com comentários git (
#), então ogit commita exibe no editor. O usuário pode modificá-la ou descartá-la.
A geração:
let session = LanguageModelSession(instructions: """
You generate git commit messages in conventional commits format.
Focus on WHY the change was made, not WHAT changed.
The subject must be imperative mood, lowercase, no period, max 50 chars.
""")
let result = try await session.respond(to: prompt, generating: CommitMessage.self)
let msg = result.content
let message = "\(msg.type)(\(msg.scope)): \(msg.subject)"
session.respond(to:generating:) retorna uma instância de CommitMessage, não uma String. Não há parsing. Não há regex. Não há try? JSONDecoder().decode(...). A struct é o contrato, e o compilador o garante.
Integração com rastreamento de issues: fm-lql-create
O mesmo padrão funciona para rastreamento de issues. fm-lql-create classifica uma descrição em linguagem natural e cria uma issue no Linear via lql, um CLI do Linear escrito em Rust:
@Generable
struct IssueClassification {
@Guide(.anyOf(["bug", "feature", "improvement", "task", "chore"]))
var type: String
@Guide(.anyOf(["urgent", "high", "medium", "low", "none"]))
var priority: String
@Guide(description: "Clean, professional issue title. Max 80 chars.")
var title: String
@Guide(description: "One-line description for the issue body")
var description: String
}
Uso:
$ fm-lql-create "auth token refresh crashes when expired"
PROD | high | bug | TOK: Auth: token refresh crashes on expiry
Token refresh fails silently when the OAuth token has expired, causing auth loop.
Press Enter to create, Ctrl-C to cancel:
O modelo local classifica a issue em ~500ms: tipo bug, prioridade alta, título limpo, descrição de uma linha. Depois, lql create a adiciona ao Linear. O flag --dry-run exibe a proposta sem executar nada.
Dois campos com anyOf (type, priority) garantem que a classificação seja válida. Ele não pode retornar “priority: muito importante” nem “type: bugfix”. Os tokens estão mascarados. Dois campos com description (title, description) dão controle limitado ao modelo.
Antes e depois
| Passo | Com coding agent (Opus) | Com foundation-hooks |
|---|---|---|
| Gerar mensagem de commit | ~2s, ~800 tokens, ~$0.014 | ~300ms, 0 tokens, $0.00 |
| Validar formato | ~1.5s, ~300 tokens, ~$0.006 | ~200ms, 0 tokens, $0.00 |
| Classificar issue | ~2s, ~500 tokens, ~$0.011 | ~500ms, 0 tokens, $0.00 |
| Gerar resumo de stand-up | ~3s, ~2000 tokens, ~$0.045 | ~800ms, 0 tokens, $0.00 |
| Requer rede | Sim | Não |
| Requer chave API | Sim | Não |
| Funciona offline | Não | Sim |
Os tempos do modelo local são medições reais em um MacBook Pro M4 Pro. Não são benchmarks sintéticos.
O que ele não consegue fazer
O modelo on-device da Apple é um modelo de 3B parâmetros com uma janela de contexto de 4096 tokens. Possui limites claros:
Diffs longos: acima de ~3000 caracteres de patch, o contexto é truncado. Para grandes refactors que tocam 20 arquivos, o modelo vê apenas o resumo estatístico (
--stat), não o patch completo. A mensagem de commit será genérica, mas correta quanto ao formato.Decisões arquitetônicas: “Devo usar um protocolo ou um tipo concreto aqui?” é uma pergunta que exige contexto de projeto, histórico do codebase e raciocínio multi-passo. Isso ainda é território de modelos maiores.
Geração de código: foundation-hooks não gera código. Ele gera metadata sobre código: mensagens de commit, classificações, resumos. A fronteira é clara: se a tarefa é “escrever” algo que será revisado por um humano, use o modelo grande. Se a tarefa é “etiquetar” algo que um humano já escreveu, use o modelo local.
Apenas macOS 26+ em Apple Silicon: não funciona em Linux, nem em Macs Intel. Para equipes heterogêneas, o hook sai silenciosamente e o usuário escreve sua própria mensagem.
Instalação
# Pré-requisitos: macOS 26, Xcode 26, Apple Intelligence ativado
git clone https://github.com/frr149/foundation-hooks
cd foundation-hooks
make build
# Instalar hooks em um repo específico
make install-hooks REPO=/path/to/your/repo
# Instalar binários CLI em ~/.local/bin
make install-lql
# Instalar hooks em todos os repos conhecidos (editar Makefile para ajustar a lista)
make install-all
O Makefile copia os binários compilados diretamente para .git/hooks/. Não há runtime, daemon ou configuração. Se o binário está no hook, ele funciona. Se não quiser AI em um commit, git commit --no-verify.
A tese
Os agentes de codificação são ferramentas extraordinárias para tarefas que requerem raciocínio complexo. Mas o modelo de preços atual não diferencia entre complexidade. Cada interação com o modelo – desde projetar uma arquitetura até escrever “fix: typo” – passa pelo mesmo pipeline, ao mesmo custo e com a mesma latência.
A solução não é parar de usar agentes de codificação. É parar de usá-los para tudo. Tarefas de classificação, validação e geração com restrições rígidas são solucionáveis com um modelo de 3B parâmetros rodando localmente. O hardware já está na sua máquina. O framework já está no sistema operacional. Falta apenas o código que os conecte.
foundation-hooks são 400 linhas de Swift que conectam esses pontos. make install-hooks REPO=. e cada commit gera sua própria mensagem, cada issue é classificada automaticamente, cada stand-up se escreve em 800ms. Sem rede, sem tokens, sem custo.
O cirurgião pode deixar de colocar band-aids.