<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Indiorlei de Oliveira</title>
    <description>The latest articles on DEV Community by Indiorlei de Oliveira (@indiorlei).</description>
    <link>https://dev.to/indiorlei</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F226592%2Fffded7d6-1776-4f75-801b-4445833921a1.png</url>
      <title>DEV Community: Indiorlei de Oliveira</title>
      <link>https://dev.to/indiorlei</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/indiorlei"/>
    <language>en</language>
    <item>
      <title>O Calibre não enxergava o meu OCR: como fiz um app para Mac que leva PDFs escaneados ao Kindle</title>
      <dc:creator>Indiorlei de Oliveira</dc:creator>
      <pubDate>Thu, 12 Nov 2026 14:30:33 +0000</pubDate>
      <link>https://dev.to/indiorlei/o-calibre-nao-enxergava-o-meu-ocr-como-fiz-um-app-para-mac-que-leva-pdfs-escaneados-ao-kindle-4i4p</link>
      <guid>https://dev.to/indiorlei/o-calibre-nao-enxergava-o-meu-ocr-como-fiz-um-app-para-mac-que-leva-pdfs-escaneados-ao-kindle-4i4p</guid>
      <description>&lt;p&gt;Eu leio muito no Kindle, e boa parte do que quero ler só existe em PDF. Alguns são exportados direitinho, com texto de verdade. Muitos são livros escaneados: 400 fotos de páginas, 180 MB, meio tortas.&lt;/p&gt;

&lt;p&gt;Dá para mandar o PDF direto para o Kindle, mas o resultado é ruim. O PDF tem página fixa, então numa tela de 6" a letra fica minúscula e não dá para aumentar. O que você quer é um &lt;strong&gt;EPUB&lt;/strong&gt;, que se ajusta à tela e deixa trocar a fonte e o tamanho da letra, como um livro comprado na loja.&lt;/p&gt;

&lt;p&gt;A receita de sempre usa quatro ferramentas: uma para o OCR, o Calibre para converter, alguma coisa para diminuir as imagens e o e-mail ou o app da Amazon para enviar. Eu queria uma janela só, onde arrasto o PDF e o livro aparece no Kindle.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvm6qt9vk6lmj4aevnygv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvm6qt9vk6lmj4aevnygv.png" alt="A fila do Lumink: cada livro mostra a etapa em que está" width="799" height="629"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  O pipeline
&lt;/h2&gt;

&lt;p&gt;Cada PDF entra numa fila e passa pelas mesmas cinco etapas, um livro de cada vez:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PDF ─▶ Análise ─▶ OCR ─────────────▶ Conversão ─▶ Compressão ─▶ Envio
       PDFKit     ocrmypdf ou Vision  Calibre      ImageIO        Send to Kindle
                  (só se escaneado)   ou Modo imagem              ou SMTP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vou passar por cada uma, mas a parte interessante é a terceira.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Análise: esse PDF tem texto, é escaneado ou os dois?
&lt;/h3&gt;

&lt;p&gt;Rodar OCR num PDF que já tem texto gasta minutos e ainda pode piorar o resultado. Então o primeiro passo é descobrir o que temos em mãos.&lt;/p&gt;

&lt;p&gt;O Lumink usa o PDFKit para extrair o texto de até 30 páginas espalhadas pelo livro. Uma página "tem texto" se tiver 40 caracteres ou mais, sem contar espaços:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;ratio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Double&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;withText&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="kt"&gt;Double&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PDFKind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;ratio&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="c1"&gt;// PDF com texto: pula o OCR&lt;/span&gt;
    &lt;span class="n"&gt;ratio&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;  &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scanned&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;// escaneado&lt;/span&gt;
                   &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mixed&lt;/span&gt;      &lt;span class="c1"&gt;// misto&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Como só uma amostra é lida, essa etapa leva menos de um segundo, mesmo num livro de 600 páginas. Quando o OCR roda de verdade, aí sim o Lumink verifica todas as páginas e só reconhece as que estão com menos de 40 caracteres.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. OCR, com plano B
&lt;/h3&gt;

&lt;p&gt;Nos PDFs escaneados e mistos, o Lumink roda o &lt;a href="https://github.com/ocrmypdf/OCRmyPDF" rel="noopener noreferrer"&gt;&lt;code&gt;ocrmypdf&lt;/code&gt;&lt;/a&gt; (Tesseract) com &lt;code&gt;--skip-text&lt;/code&gt;, para não refazer as páginas que já têm texto.&lt;/p&gt;

&lt;p&gt;O ocrmypdf é opcional. Se ele não estiver instalado ou falhar, o Lumink usa o &lt;strong&gt;Vision&lt;/strong&gt; da Apple (&lt;code&gt;VNRecognizeTextRequest&lt;/code&gt; no modo &lt;em&gt;accurate&lt;/em&gt;), que já vem no macOS. É mais lento, mas o OCR funciona até num Mac sem nada instalado.&lt;/p&gt;

&lt;p&gt;Se um PDF escaneado render menos de 100 caracteres por página, em média, depois do OCR, provavelmente é uma HQ ou um livro de fotos. Nesse caso o Lumink muda sozinho para o &lt;strong&gt;Modo imagem&lt;/strong&gt; (falo dele mais abaixo).&lt;/p&gt;

&lt;h3&gt;
  
  
  3. O bug: o Calibre ignora texto invisível
&lt;/h3&gt;

&lt;p&gt;Foi essa parte que me custou uma noite.&lt;/p&gt;

&lt;p&gt;O OCR não substitui a imagem escaneada. Ele mantém a imagem e põe por cima uma &lt;strong&gt;camada de texto invisível&lt;/strong&gt;, posicionada sobre as palavras. No PDF, isso é texto com &lt;em&gt;render mode 3&lt;/em&gt; (um modo de pintura que não desenha nada). É por isso que dá para selecionar e buscar texto num PDF com OCR e continuar vendo o scan original.&lt;/p&gt;

&lt;p&gt;Passei esse PDF para o &lt;code&gt;ebook-convert&lt;/code&gt; do Calibre, e o EPUB saiu... sem texto nenhum. Só imagens, uma por página.&lt;/p&gt;

&lt;p&gt;Testei o outro motor de PDF do Calibre, e deu o mesmo resultado. Os dois &lt;strong&gt;descartam texto invisível&lt;/strong&gt;. Para o Calibre, uma página com OCR é uma foto sem nada escrito.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A solução:&lt;/strong&gt; montar eu mesmo o PDF que vai para o Calibre (&lt;code&gt;TextLayerPDFBuilder&lt;/code&gt; no código). São dois passos.&lt;/p&gt;

&lt;p&gt;Primeiro, ler de volta o texto que o OCR gravou, linha por linha e com a posição de cada uma. O PDFKit faz isso direto da camada invisível:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;box&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bounds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mediaBox&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;selectionsByLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="c1"&gt;// cada linha: texto + retângulo na página&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Depois, gerar um PDF novo. As páginas que já tinham texto são copiadas como estão. Nas páginas escaneadas, cada linha é desenhada como texto &lt;strong&gt;visível&lt;/strong&gt;, no mesmo lugar, e a imagem do scan fica de fora:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A caixa da linha inclui ascendentes e descendentes; ~75% dela é o corpo da fonte.&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;fontSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="c1"&gt;// Estica ou encolhe na horizontal para ocupar a largura original da linha.&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;sx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;textMatrix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CGAffineTransform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scaleX&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;textPosition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CGPoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minY&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;descent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;CTLineDraw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctLine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esse esticamento horizontal parece detalhe, mas faz diferença. O Calibre decide se duas linhas fazem parte do mesmo parágrafo olhando onde cada uma termina. Se a linha redesenhada ficasse mais curta que a original, ele quebraria o parágrafo no meio.&lt;/p&gt;

&lt;p&gt;O resultado é que o Calibre enxerga texto de verdade e o reflow funciona. De bônus, o EPUB não carrega uma foto de cada página, o que o deixa muito menor.&lt;/p&gt;

&lt;p&gt;O custo é que as figuras dentro de páginas &lt;em&gt;escaneadas&lt;/em&gt; não vão para o EPUB. Para livros que são quase só ilustração, o Modo imagem é a melhor escolha.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Conversão com o Calibre
&lt;/h3&gt;

&lt;p&gt;Para texto, o Lumink chama o &lt;code&gt;ebook-convert&lt;/code&gt; com opções pensadas para livros, não para documentos. Esta é a parte que importa:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ebook-convert entrada.pdf saida.epub \
  --output-profile kindle_pw3 \
  --pdf-engine calibre \
  --pdf-header-skip -1 --pdf-footer-skip -1 \
  --pdf-footer-regex &lt;span class="s1"&gt;'^\s*[-–—]?\s*\d{1,4}\s*[-–—]?\s*$'&lt;/span&gt; \
  --unwrap-factor 0.45 \
  --enable-heuristics \
  --filter-css font-family,color,background-color \
  --epub-version 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--pdf-header-skip&lt;/code&gt;/&lt;code&gt;--pdf-footer-skip -1&lt;/code&gt; detectam e tiram os títulos que se repetem no topo e no pé de toda página.&lt;/li&gt;
&lt;li&gt;A regex tira os números de página soltos, inclusive os do tipo &lt;code&gt;— 42 —&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--enable-heuristics&lt;/code&gt; junta linhas quebradas e desfaz a hifenização.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--filter-css&lt;/code&gt; tira a fonte e as cores do PDF, e o Kindle aplica as que o leitor escolheu.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sem essas opções, o EPUB parece uma fotocópia, com um número de página no meio da frase a cada poucos parágrafos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Um detalhe sobre o idioma:&lt;/strong&gt; o Lumink detecta o idioma do livro com o &lt;code&gt;NLLanguageRecognizer&lt;/code&gt; (do texto do PDF ou do texto do OCR) e grava em &lt;code&gt;dc:language&lt;/code&gt;, nos metadados do EPUB. Parece cosmético, mas o Kindle usa esse campo para escolher o dicionário, a hifenização e a voz da leitura em voz alta. O mesmo idioma vai na frente da lista de idiomas do OCR, porque tanto o Vision quanto o Tesseract dão mais peso ao primeiro.&lt;/p&gt;

&lt;h3&gt;
  
  
  Modo imagem, para HQ e mangá
&lt;/h3&gt;

&lt;p&gt;HQs, mangás e livros de fotos não têm texto que valha ajustar à tela. O Modo imagem pula o OCR e o Calibre e gera direto um &lt;strong&gt;EPUB 3 de layout fixo&lt;/strong&gt; (&lt;code&gt;rendition:layout&lt;/code&gt; igual a &lt;code&gt;pre-paginated&lt;/code&gt;), com uma imagem por página. É o único modo que não precisa do Calibre.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Compressão
&lt;/h3&gt;

&lt;p&gt;Livros escaneados são pesados, e as duas formas de envio têm limite de tamanho. Então o Lumink descompacta o EPUB e recomprime as imagens em paralelo com o ImageIO:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Nível&lt;/th&gt;
&lt;th&gt;Altura máxima&lt;/th&gt;
&lt;th&gt;Qualidade do JPEG&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Alta&lt;/td&gt;
&lt;td&gt;1600 px&lt;/td&gt;
&lt;td&gt;85%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Média&lt;/td&gt;
&lt;td&gt;1600 px&lt;/td&gt;
&lt;td&gt;75%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Máxima&lt;/td&gt;
&lt;td&gt;1200 px&lt;/td&gt;
&lt;td&gt;55%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Além disso, os PNGs opacos de 40 KB ou mais viram JPEG, depois de conferir a transparência pixel a pixel. E uma imagem nova só fica se for mesmo menor que a original.&lt;/p&gt;

&lt;p&gt;Quando um PNG vira JPEG, as referências no HTML, no CSS e no OPF, e também o &lt;code&gt;media-type&lt;/code&gt;, são atualizados. Depois o app compacta o livro de novo, com o &lt;code&gt;mimetype&lt;/code&gt; como primeira entrada e sem compressão, como a especificação do EPUB exige (&lt;code&gt;zip -X -0&lt;/code&gt;). Se essa regra não for seguida, alguns leitores recusam o arquivo.&lt;/p&gt;

&lt;p&gt;Se o livro ainda passar do limite, ele tenta o nível seguinte. Se nem a compressão máxima resolver, salva o EPUB e avisa, em vez de mandar um arquivo que vai voltar.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Envio, sem pedir a sua senha
&lt;/h3&gt;

&lt;p&gt;A primeira versão enviava os livros por e-mail, o que significava pedir uma senha SMTP. Mesmo guardada no Keychain, eu não gostava de pedir senha para ninguém.&lt;/p&gt;

&lt;p&gt;Agora o padrão é o app oficial &lt;strong&gt;&lt;a href="https://www.amazon.com/sendtokindle" rel="noopener noreferrer"&gt;Send to Kindle&lt;/a&gt;&lt;/strong&gt; da Amazon para Mac. O Lumink abre o EPUB nele, você escolhe o Kindle e confirma. O login fica com a Amazon: nenhuma senha no Lumink, nenhum servidor no meio, e um limite de 200 MB.&lt;/p&gt;

&lt;p&gt;O e-mail continua como opção, para quem quer envio totalmente automático. Dois detalhes que gostei de resolver ali:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A conta do tamanho.&lt;/strong&gt; O Send to Kindle por e-mail aceita 50 MB, mas o Gmail recusa mensagens acima de 25 MB, e o base64 aumenta o anexo em uns 33%. Sobram uns 18 MB de anexo. Quando o servidor é o Gmail, o Lumink usa 18 MB como limite automaticamente.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Por que &lt;code&gt;curl&lt;/code&gt;.&lt;/strong&gt; Eu poderia usar um pacote Swift de SMTP, mas a maioria mal é mantida, e cada um traria o próprio código de TLS e autenticação para eu auditar. Em vez disso, o Lumink monta a mensagem MIME e entrega para o &lt;code&gt;curl&lt;/code&gt; que já vem no macOS. A senha vai para o curl pelo &lt;strong&gt;stdin&lt;/strong&gt; (&lt;code&gt;--config -&lt;/code&gt;), então nunca aparece no &lt;code&gt;ps&lt;/code&gt; nem num arquivo no disco.&lt;/p&gt;

&lt;h2&gt;
  
  
  O que aprendi
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Teste a ferramenta com a entrada real, não com a que você imagina.&lt;/strong&gt; Eu testava o Calibre com PDFs de texto, e funcionava. O problema só apareceu com PDFs saídos do OCR, porque "ter texto" e "ter texto visível" são coisas diferentes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plano B nativo vale a pena.&lt;/strong&gt; Ter o Vision como alternativa ao ocrmypdf fez o app funcionar num Mac novo, sem Homebrew e sem Python.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Às vezes o recurso mais seguro é o que não existe.&lt;/strong&gt; Tirar a senha do caminho padrão resolveu mais do que qualquer cuidado com o Keychain.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  O que ele ainda não faz
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Não tem leitura da direita para a esquerda para mangá japonês: as páginas ficam na ordem do PDF.&lt;/li&gt;
&lt;li&gt;Figuras dentro de páginas escaneadas são descartadas (veja a parte do Calibre).&lt;/li&gt;
&lt;li&gt;O OCR do Vision é bem mais lento que o do ocrmypdf, principalmente na primeira vez, quando carrega os modelos.&lt;/li&gt;
&lt;li&gt;Ainda não é notarizado pela Apple. Na primeira abertura, é preciso ir em Ajustes do Sistema → Privacidade e Segurança → Abrir mesmo assim, ou rodar um comando &lt;code&gt;xattr&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Experimente
&lt;/h2&gt;

&lt;p&gt;O Lumink é grátis e de código aberto, para macOS 14 ou mais novo, em Macs Apple Silicon e Intel. A interface está em português e em inglês. Para converter livros com texto, você precisa ter o &lt;a href="https://calibre-ebook.com" rel="noopener noreferrer"&gt;Calibre&lt;/a&gt; instalado. O ocrmypdf é opcional.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Site e download: &lt;strong&gt;&lt;a href="https://lumink-app.vercel.app" rel="noopener noreferrer"&gt;https://lumink-app.vercel.app&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Código: &lt;strong&gt;&lt;a href="https://github.com/indiorlei/lumink-app" rel="noopener noreferrer"&gt;https://github.com/indiorlei/lumink-app&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Se você lê PDFs no Kindle, quero saber o que quebra. Issues e pull requests são bem-vindos, e o código é pequeno o bastante para ler numa tarde.&lt;/p&gt;

&lt;p&gt;E fica a pergunta: você já esbarrou em alguma ferramenta que ignorava uma parte "invisível" do arquivo? Conta nos comentários.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Por que o nome? **Lume&lt;/em&gt;* é uma palavra antiga do português: fogo, chama, luz (do latim* lumen*), como em "vir a lume". &lt;strong&gt;Ink&lt;/strong&gt; é tinta em inglês, como na tela de tinta eletrônica do leitor. E, em inglês,* kindle &lt;em&gt;quer dizer acender.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Read this post in English: &lt;a href="https://dev.to/indiorlei/calibre-couldnt-see-my-ocr-building-a-mac-app-that-gets-scanned-pdfs-onto-a-kindle-26bi"&gt;Calibre couldn't see my OCR: building a Mac app that gets scanned PDFs onto a Kindle&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>braziliandevs</category>
      <category>macos</category>
      <category>swift</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Calibre couldn't see my OCR: building a Mac app that gets scanned PDFs onto a Kindle</title>
      <dc:creator>Indiorlei de Oliveira</dc:creator>
      <pubDate>Thu, 12 Nov 2026 14:30:27 +0000</pubDate>
      <link>https://dev.to/indiorlei/calibre-couldnt-see-my-ocr-building-a-mac-app-that-gets-scanned-pdfs-onto-a-kindle-26bi</link>
      <guid>https://dev.to/indiorlei/calibre-couldnt-see-my-ocr-building-a-mac-app-that-gets-scanned-pdfs-onto-a-kindle-26bi</guid>
      <description>&lt;p&gt;I read a lot on a Kindle, and a lot of what I want to read only exists as a PDF. Some are clean exports with real text. Many are scanned books: 400 photos of pages, 180 MB, slightly crooked.&lt;/p&gt;

&lt;p&gt;You can send a PDF straight to a Kindle, but the result is bad. A PDF has fixed pages, so on a 6" screen the text is tiny and you can't make it bigger. What you want is an &lt;strong&gt;EPUB&lt;/strong&gt;, which reflows to fit the screen and lets you change the font and text size, like a book bought from the store.&lt;/p&gt;

&lt;p&gt;The usual recipe takes four tools: one for OCR, Calibre to convert, something to shrink the images, then email or Amazon's app to send. I wanted one window where I drop the PDF and the book shows up on my Kindle.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8399xgs4k9rntcc490lq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8399xgs4k9rntcc490lq.png" alt="The Lumink queue: each book shows the stage it's in" width="799" height="629"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline
&lt;/h2&gt;

&lt;p&gt;Every PDF goes into a queue and through the same five stages, one book at a time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PDF ─▶ Analyze ─▶ OCR ─────────────▶ Convert ─▶ Compress ─▶ Send
       PDFKit     ocrmypdf or Vision  Calibre    ImageIO      Send to Kindle
                  (only if scanned)   or image mode           or SMTP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'll go through each one, but the interesting part is the third.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Analyze: is this PDF text, scanned, or both?
&lt;/h3&gt;

&lt;p&gt;Running OCR on a PDF that already has text wastes minutes and can make things worse, so the first step is to find out what we have.&lt;/p&gt;

&lt;p&gt;Lumink uses PDFKit to pull the text from up to 30 pages spread across the book. A page "has text" if it has 40 or more non-whitespace characters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;ratio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Double&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;withText&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="kt"&gt;Double&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PDFKind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;ratio&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="c1"&gt;// text PDF: skip OCR&lt;/span&gt;
    &lt;span class="n"&gt;ratio&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;  &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scanned&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;// scanned&lt;/span&gt;
                   &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mixed&lt;/span&gt;      &lt;span class="c1"&gt;// mixed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because it only reads a sample, this step takes under a second, even on a 600-page book. When OCR does run, Lumink checks every page and only processes the ones with fewer than 40 characters.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. OCR, with a fallback
&lt;/h3&gt;

&lt;p&gt;For scanned and mixed PDFs, Lumink runs &lt;a href="https://github.com/ocrmypdf/OCRmyPDF" rel="noopener noreferrer"&gt;&lt;code&gt;ocrmypdf&lt;/code&gt;&lt;/a&gt; (Tesseract) with &lt;code&gt;--skip-text&lt;/code&gt;, so pages that already have text are left alone.&lt;/p&gt;

&lt;p&gt;ocrmypdf is optional. If it isn't installed or it fails, Lumink falls back to Apple's &lt;strong&gt;Vision&lt;/strong&gt; framework (&lt;code&gt;VNRecognizeTextRequest&lt;/code&gt; in accurate mode), which ships with macOS. It's slower, but it means OCR works on a fresh Mac with nothing installed.&lt;/p&gt;

&lt;p&gt;If a scanned PDF yields fewer than 100 characters per page on average after OCR, it's probably a comic or a photo book. In that case Lumink switches to &lt;strong&gt;image mode&lt;/strong&gt; on its own (more on that below).&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The bug: Calibre ignores invisible text
&lt;/h3&gt;

&lt;p&gt;This is the part that cost me an evening.&lt;/p&gt;

&lt;p&gt;OCR doesn't replace the scanned image. It keeps the image and adds an &lt;strong&gt;invisible text layer&lt;/strong&gt; on top, positioned over the words. In PDF terms, that's text drawn with &lt;em&gt;render mode 3&lt;/em&gt;, a paint mode that draws nothing. That's how you can select and search text in an OCR'd PDF while still seeing the original scan.&lt;/p&gt;

&lt;p&gt;I fed that PDF to Calibre's &lt;code&gt;ebook-convert&lt;/code&gt;, and the EPUB came out... with no text at all. Just images, one per page.&lt;/p&gt;

&lt;p&gt;I tried Calibre's other PDF engine and got the same result. Both &lt;strong&gt;drop invisible text&lt;/strong&gt;. As far as Calibre is concerned, an OCR'd page is a picture with nothing written on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; build the PDF that goes to Calibre myself (&lt;code&gt;TextLayerPDFBuilder&lt;/code&gt; in the code). It takes two steps.&lt;/p&gt;

&lt;p&gt;First, read back the text the OCR wrote, line by line, with each line's position. PDFKit reads it straight from the invisible layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;box&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bounds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mediaBox&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;selectionsByLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="c1"&gt;// each line: text + rectangle on the page&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, write a new PDF. Pages that already had text are copied as they are. On scanned pages, every line is drawn as &lt;strong&gt;visible&lt;/strong&gt; text in the same spot, and the scan image is left out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The line box includes ascenders and descenders; ~75% of it is the font size.&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;fontSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="c1"&gt;// Stretch or squeeze horizontally to fill the line's original width.&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;sx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;textMatrix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CGAffineTransform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scaleX&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;textPosition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CGPoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minY&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;descent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;CTLineDraw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctLine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The horizontal stretch looks like a detail, but it matters. Calibre decides whether two lines belong to the same paragraph by looking at where each line ends. If a redrawn line came out shorter than the original, Calibre would break the paragraph in the middle.&lt;/p&gt;

&lt;p&gt;Now Calibre sees real text, and reflow works. As a bonus, the EPUB doesn't carry a photo of every page, which makes it far smaller.&lt;/p&gt;

&lt;p&gt;The trade-off is that figures inside &lt;em&gt;scanned&lt;/em&gt; pages don't make it into the EPUB. For books that are mostly illustrations, image mode is the better choice.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Convert with Calibre
&lt;/h3&gt;

&lt;p&gt;For text, Lumink calls &lt;code&gt;ebook-convert&lt;/code&gt; with options tuned for books rather than documents. Here are the ones that matter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ebook-convert input.pdf output.epub \
  --output-profile kindle_pw3 \
  --pdf-engine calibre \
  --pdf-header-skip -1 --pdf-footer-skip -1 \
  --pdf-footer-regex &lt;span class="s1"&gt;'^\s*[-–—]?\s*\d{1,4}\s*[-–—]?\s*$'&lt;/span&gt; \
  --unwrap-factor 0.45 \
  --enable-heuristics \
  --filter-css font-family,color,background-color \
  --epub-version 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--pdf-header-skip&lt;/code&gt;/&lt;code&gt;--pdf-footer-skip -1&lt;/code&gt; detect and remove the running titles at the top and bottom of every page.&lt;/li&gt;
&lt;li&gt;The regex removes stray page numbers, including ones like &lt;code&gt;— 42 —&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--enable-heuristics&lt;/code&gt; joins broken lines and undoes hyphenation.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--filter-css&lt;/code&gt; strips the PDF's fonts and colors, so the Kindle applies the ones the reader picked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without these options, the EPUB reads like a photocopy, with a page number in the middle of a sentence every few paragraphs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A note on language:&lt;/strong&gt; Lumink detects the book's language with &lt;code&gt;NLLanguageRecognizer&lt;/code&gt; (from the PDF's text or from the OCR output) and writes it to &lt;code&gt;dc:language&lt;/code&gt; in the EPUB metadata. It looks cosmetic, but the Kindle uses that field to pick the dictionary, hyphenation and the text-to-speech voice. The same language also goes to the front of the OCR language list, because both Vision and Tesseract give more weight to the first entry.&lt;/p&gt;

&lt;h3&gt;
  
  
  Image mode, for comics and manga
&lt;/h3&gt;

&lt;p&gt;Comics, manga and photo books don't have text worth reflowing. Image mode skips OCR and Calibre and builds a &lt;strong&gt;fixed-layout EPUB 3&lt;/strong&gt; (&lt;code&gt;rendition:layout&lt;/code&gt; set to &lt;code&gt;pre-paginated&lt;/code&gt;) directly, one image per page. It's the only mode that doesn't need Calibre at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Compress
&lt;/h3&gt;

&lt;p&gt;Scanned books are heavy, and both delivery paths have size limits. So Lumink unzips the EPUB and recompresses the images in parallel with ImageIO:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Max height&lt;/th&gt;
&lt;th&gt;JPEG quality&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;1600 px&lt;/td&gt;
&lt;td&gt;85%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;1600 px&lt;/td&gt;
&lt;td&gt;75%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maximum&lt;/td&gt;
&lt;td&gt;1200 px&lt;/td&gt;
&lt;td&gt;55%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On top of that, opaque PNGs of 40 KB or more become JPEGs, after a pixel-by-pixel transparency check. A new image is only kept if it's actually smaller than the original.&lt;/p&gt;

&lt;p&gt;When a PNG becomes a JPEG, the references in the HTML, CSS and OPF, and the &lt;code&gt;media-type&lt;/code&gt;, are updated too. Then the app zips the book back up, with &lt;code&gt;mimetype&lt;/code&gt; as the first, uncompressed entry, as the EPUB spec requires (&lt;code&gt;zip -X -0&lt;/code&gt;). Some readers reject the file if that rule isn't followed.&lt;/p&gt;

&lt;p&gt;If the book is still over the limit, it tries the next level. If even maximum compression doesn't fit, it saves the EPUB and tells you, instead of sending a file that would bounce.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Send, without asking for your password
&lt;/h3&gt;

&lt;p&gt;The first version sent books by email, which meant asking for an SMTP password. Even stored in the Keychain, I didn't like asking anyone for a password.&lt;/p&gt;

&lt;p&gt;The default now is Amazon's own &lt;strong&gt;&lt;a href="https://www.amazon.com/sendtokindle" rel="noopener noreferrer"&gt;Send to Kindle&lt;/a&gt;&lt;/strong&gt; app for Mac. Lumink opens the EPUB in it, and you pick your Kindle and confirm. The login stays with Amazon: no password in Lumink, no server in between, and a 200 MB limit.&lt;/p&gt;

&lt;p&gt;Email is still there as an option for fully automatic delivery. Two details I liked solving there:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The size math.&lt;/strong&gt; Send to Kindle by email accepts 50 MB, but Gmail rejects messages over 25 MB, and base64 makes the attachment about 33% bigger. That leaves roughly 18 MB of attachment. When the server is Gmail, Lumink uses 18 MB as the limit automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;curl&lt;/code&gt;.&lt;/strong&gt; I could have used a Swift SMTP package, but most are barely maintained, and each would bring its own TLS and auth code for me to audit. Instead, Lumink builds the MIME message itself and hands it to the &lt;code&gt;curl&lt;/code&gt; that ships with macOS. The password goes to curl through &lt;strong&gt;stdin&lt;/strong&gt; (&lt;code&gt;--config -&lt;/code&gt;), so it never shows up in &lt;code&gt;ps&lt;/code&gt; or in a file on disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Test the tool with the real input, not the one you picture.&lt;/strong&gt; I tested Calibre with text PDFs, and it worked. The problem only showed up with PDFs that came out of OCR, because "has text" and "has visible text" are different things.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A native fallback is worth it.&lt;/strong&gt; Having Vision as a fallback for ocrmypdf made the app work on a fresh Mac, with no Homebrew and no Python.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sometimes the safest feature is the one you remove.&lt;/strong&gt; Taking the password out of the default path did more than any amount of Keychain care.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Things it doesn't do (yet)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;No right-to-left reading for Japanese manga: pages stay in PDF order.&lt;/li&gt;
&lt;li&gt;Figures inside scanned pages are dropped (see the Calibre section).&lt;/li&gt;
&lt;li&gt;Vision OCR is much slower than ocrmypdf, especially the first time, when it loads its models.&lt;/li&gt;
&lt;li&gt;It isn't notarized by Apple yet, so on first launch you need to go to System Settings → Privacy &amp;amp; Security → Open Anyway, or run one &lt;code&gt;xattr&lt;/code&gt; command.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Lumink is free and open source, for macOS 14 or later, on Apple Silicon and Intel. The interface is in English and Portuguese. To convert text books, you need &lt;a href="https://calibre-ebook.com" rel="noopener noreferrer"&gt;Calibre&lt;/a&gt; installed. ocrmypdf is optional.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Website and download: &lt;strong&gt;&lt;a href="https://lumink-app.vercel.app/en" rel="noopener noreferrer"&gt;https://lumink-app.vercel.app/en&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Source: &lt;strong&gt;&lt;a href="https://github.com/indiorlei/lumink-app" rel="noopener noreferrer"&gt;https://github.com/indiorlei/lumink-app&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you read PDFs on a Kindle, I'd love to hear what breaks. Issues and pull requests are welcome, and the code is small enough to read in an afternoon.&lt;/p&gt;

&lt;p&gt;And a question for you: have you ever run into a tool that silently ignored an "invisible" part of a file? Tell me in the comments.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Why the name? **Lume&lt;/em&gt;* is an old Portuguese word for fire and light (from Latin* lumen*). &lt;strong&gt;Ink&lt;/strong&gt; is for the e-ink screen. And in English, to* kindle &lt;em&gt;means to set alight.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Leia este post em português: &lt;a href="https://dev.to/indiorlei/o-calibre-nao-enxergava-o-meu-ocr-como-fiz-um-app-para-mac-que-leva-pdfs-escaneados-ao-kindle-4i4p"&gt;O Calibre não enxergava o meu OCR: como fiz um app para Mac que leva PDFs escaneados ao Kindle&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>macos</category>
      <category>swift</category>
      <category>kindle</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Como transformei tarefas repetitivas de Git em agentes de IA com o Kiro CLI</title>
      <dc:creator>Indiorlei de Oliveira</dc:creator>
      <pubDate>Sun, 11 Oct 2026 13:41:17 +0000</pubDate>
      <link>https://dev.to/indiorlei/como-transformei-tarefas-repetitivas-de-git-em-agentes-de-ia-com-o-kiro-cli-3d8f</link>
      <guid>https://dev.to/indiorlei/como-transformei-tarefas-repetitivas-de-git-em-agentes-de-ia-com-o-kiro-cli-3d8f</guid>
      <description>&lt;p&gt;Toda equipe tem aquele conjunto de tarefas chatas que ninguém gosta de fazer, mas que precisam ser feitas sempre do mesmo jeito: abrir PR com o título no padrão, nomear a branch certo, montar o changelog, linkar o work item. São tarefas de baixo valor intelectual e alto custo de atenção — e, pior, qualquer desvio do padrão vira ruído na revisão.&lt;/p&gt;

&lt;p&gt;Resolvi atacar isso de uma forma diferente: em vez de escrever mais um script bash gigante, montei &lt;strong&gt;agentes de IA especializados&lt;/strong&gt; que encapsulam esses fluxos. Neste artigo mostro como estruturei esse setup usando o &lt;strong&gt;Kiro CLI&lt;/strong&gt;, com um padrão que separa claramente &lt;em&gt;configuração&lt;/em&gt;, &lt;em&gt;conhecimento&lt;/em&gt; e &lt;em&gt;regras de segurança&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Vou usar meus próprios agentes como exemplo — abstraindo nomes internos — mas o padrão serve para qualquer time.&lt;/p&gt;

&lt;h2&gt;
  
  
  O problema
&lt;/h2&gt;

&lt;p&gt;No meu dia a dia eu repetia, várias vezes por dia, dois fluxos:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Abrir um Pull Request&lt;/strong&gt;: criar a branch no padrão, commitar com a convenção certa, inferir a branch de destino, abrir o PR com título e descrição padronizados e linkar o work item.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fechar um pacote de release&lt;/strong&gt;: buscar os PRs já concluídos de uma demanda, gerar o changelog, commitar e abrir um PR de release para a branch certa.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Nenhum desses passos é difícil. O problema é que todos eles têm &lt;em&gt;detalhes&lt;/em&gt; — e detalhe esquecido é retrabalho. Automatizar com um script puro seria frágil, porque boa parte das decisões depende de contexto (qual branch, qual título, qual destino).&lt;/p&gt;

&lt;p&gt;Agente de IA é bom justamente nessa zona cinzenta: segue regras, mas interpreta contexto.&lt;/p&gt;

&lt;h2&gt;
  
  
  A ideia central: separar configuração, conhecimento e regras
&lt;/h2&gt;

&lt;p&gt;O que mais me ajudou foi não jogar tudo em um prompt gigante. Em vez disso, dividi cada agente em quatro camadas, cada uma em um arquivo próprio:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.kiro/
├── agents/        # Configuração: quais ferramentas o agente pode usar
├── prompts/       # Identidade: quem o agente é e seu contexto fixo
├── steering/      # Guardrails: o que ele NUNCA pode fazer
├── skills/        # Conhecimento: o passo a passo de cada tarefa
└── settings/      # Integração com ferramentas externas (via MCP)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A sacada é que cada camada tem uma responsabilidade única:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agent&lt;/strong&gt; = o contrato (ferramentas permitidas, atalho, mensagem de boas-vindas)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt&lt;/strong&gt; = a identidade (contexto que nunca muda)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Steering&lt;/strong&gt; = os limites (proibições inegociáveis)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skill&lt;/strong&gt; = o manual (como executar cada passo, com exemplos)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Separar assim traz um benefício enorme: &lt;strong&gt;as skills viram reutilizáveis entre agentes&lt;/strong&gt;. A skill de "criar commit" é a mesma para o agente que abre PR e para o que fecha release.&lt;/p&gt;

&lt;h2&gt;
  
  
  Camada 1 — O agente (configuração)
&lt;/h2&gt;

&lt;p&gt;O arquivo de configuração do agente é enxuto. Ele diz &lt;strong&gt;o que&lt;/strong&gt; o agente pode fazer, não &lt;strong&gt;como&lt;/strong&gt;. Veja um exemplo (abstraído):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"open-pr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Agente que abre Pull Requests: cria branch, commita, abre o PR e linka o work item."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"file://./prompts/open-pr.md"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"includeMcpJson"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"shell"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"grep"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"glob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@git-provider"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"allowedTools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"shell"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"@git-provider/create_pull_request"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"@git-provider/create_branch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"@git-provider/link_work_item"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resources"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"skill://.kiro/skills/commit/SKILL.md"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"skill://.kiro/skills/create-branch/SKILL.md"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"skill://.kiro/skills/open-pr/SKILL.md"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"keyboardShortcut"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ctrl+shift+p"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"welcomeMessage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Me diga o ID da demanda ou descreva a feature. Eu cuido do resto."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dois pontos importantes aqui:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;allowedTools&lt;/code&gt; é uma allowlist explícita.&lt;/strong&gt; O agente só consegue chamar exatamente as ferramentas que eu listei. Isso é segurança por design: ele não vai, por engano, chamar uma ferramenta destrutiva que eu nunca autorizei.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;resources&lt;/code&gt; injeta as skills.&lt;/strong&gt; O agente carrega as instruções detalhadas sob demanda, em vez de carregar tudo o tempo todo.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Camada 2 — O prompt (identidade e contexto fixo)
&lt;/h2&gt;

&lt;p&gt;O prompt define quem o agente é e qual contexto nunca muda. É aqui que eu "ancoro" o agente para ele não ficar perguntando o óbvio toda vez.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Você é um agente especializado em abrir Pull Requests para o
repositório da equipe.

&lt;span class="gu"&gt;## Contexto Fixo&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Provedor de git: &lt;span class="nt"&gt;&amp;lt;configurado&lt;/span&gt; &lt;span class="na"&gt;via&lt;/span&gt; &lt;span class="na"&gt;MCP&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Projeto / repositório: &lt;span class="nt"&gt;&amp;lt;fixos&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;injetados&lt;/span&gt; &lt;span class="na"&gt;pela&lt;/span&gt; &lt;span class="na"&gt;configuração&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Branch de origem padrão: &lt;span class="sb"&gt;`main`&lt;/span&gt;

&lt;span class="gu"&gt;## Regras de Operação&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; SEMPRE usar o projeto e repositório fixos — NUNCA perguntar ao usuário
&lt;span class="p"&gt;-&lt;/span&gt; Inferir a branch de destino a partir do contexto da demanda
&lt;span class="p"&gt;-&lt;/span&gt; Seguir as convenções de branch e commit definidas nas skills

&lt;span class="gu"&gt;## O que NÃO fazer&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; NÃO perguntar projeto ou repositório — são fixos
&lt;span class="p"&gt;-&lt;/span&gt; NÃO fazer push direto para branches protegidas
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repare no padrão &lt;strong&gt;"Regras" + "O que NÃO fazer"&lt;/strong&gt;. Dizer ao agente o que &lt;em&gt;não&lt;/em&gt; fazer é tão importante quanto dizer o que fazer. Isso reduz muito o comportamento imprevisível.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Dica: dados que não devem vazar (IDs internos, GUIDs, URLs) ficam fora do artigo/documentação pública e são injetados via configuração ou MCP. Nunca coloque isso no corpo de uma skill que vai pro repositório aberto.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Camada 3 — Steering (guardrails inegociáveis)
&lt;/h2&gt;

&lt;p&gt;Essa é a camada que me deixa dormir tranquilo. O steering lista &lt;strong&gt;proibições absolutas&lt;/strong&gt;, independentes de qualquer instrução que o agente receba.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Open PR — Guardrails&lt;/span&gt;

&lt;span class="gu"&gt;## Proibições&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; NUNCA fazer force push
&lt;span class="p"&gt;-&lt;/span&gt; NUNCA push direto em branches protegidas
&lt;span class="p"&gt;-&lt;/span&gt; NUNCA criar PR sem pelo menos 1 commit na branch
&lt;span class="p"&gt;-&lt;/span&gt; NUNCA prosseguir se o git mostrar conflitos não resolvidos

&lt;span class="gu"&gt;## Confirmações Obrigatórias&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; SEMPRE mostrar o resumo (branch, commits, destino, título) antes de abrir o PR
&lt;span class="p"&gt;-&lt;/span&gt; SEMPRE confirmar se houver dúvida sobre a branch de destino

&lt;span class="gu"&gt;## Comportamento em Falha&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Se o push falhar → mostrar o erro e sugerir ação (nunca forçar)
&lt;span class="p"&gt;-&lt;/span&gt; Se a branch de destino não existir → perguntar ao usuário
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A diferença entre &lt;em&gt;prompt&lt;/em&gt; e &lt;em&gt;steering&lt;/em&gt; é intencional: o prompt é a personalidade (pode ter nuance), o steering é a lei (não tem exceção). Mesmo que o usuário peça "faz um force push aí", o guardrail segura.&lt;/p&gt;

&lt;h2&gt;
  
  
  Camada 4 — Skills (o conhecimento reutilizável)
&lt;/h2&gt;

&lt;p&gt;As skills são o coração do setup. Cada uma é um documento curto que ensina &lt;strong&gt;um&lt;/strong&gt; procedimento, com formato e exemplos. A skill de commit, por exemplo, define a convenção completa:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Responsabilidade&lt;/span&gt;
Criar commits padronizados.

&lt;span class="gu"&gt;## Formato&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;type&amp;gt;&lt;/span&gt;(&lt;span class="nt"&gt;&amp;lt;scope&amp;gt;&lt;/span&gt;): &lt;span class="nt"&gt;&amp;lt;emoji&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;descrição&lt;/span&gt; &lt;span class="na"&gt;curta&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="gu"&gt;## Procedimento&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Rodar &lt;span class="sb"&gt;`git status --short`&lt;/span&gt;
&lt;span class="p"&gt;2.&lt;/span&gt; Analisar os caminhos para determinar tipo e escopo
&lt;span class="p"&gt;3.&lt;/span&gt; Agrupar em commits lógicos
&lt;span class="p"&gt;4.&lt;/span&gt; Fazer staging seletivo (nunca &lt;span class="sb"&gt;`git add .`&lt;/span&gt;)
&lt;span class="p"&gt;5.&lt;/span&gt; Commitar com a mensagem formatada

&lt;span class="gu"&gt;## Exemplos&lt;/span&gt;
feat(module-a): ✨ add product tag component
fix(module-b): 🐛 correct price display
refactor(shared): 🔨 simplify item props
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O ponto forte: &lt;strong&gt;a mesma skill de commit é usada pelo agente de PR e pelo agente de release.&lt;/strong&gt; Escrevi uma vez, reaproveitei em todos. Quando a convenção muda, altero um arquivo só.&lt;/p&gt;

&lt;p&gt;Meu segundo agente — o de fechar release — é basicamente uma &lt;strong&gt;orquestração de skills&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Recebe o ID da demanda
  → busca os PRs já concluídos
  → troca para a branch certa
  → gera o changelog
  → usa a skill de commit
  → usa a skill de open-pr para abrir o PR de release
  → linka o work item
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ou seja: um agente complexo nasce da composição de skills simples. Isso é muito mais fácil de manter do que um prompt monolítico.&lt;/p&gt;

&lt;h2&gt;
  
  
  Camada 5 — Integração via MCP
&lt;/h2&gt;

&lt;p&gt;Para o agente conseguir &lt;em&gt;realmente&lt;/em&gt; abrir PRs e buscar demandas, ele precisa falar com o provedor de git. Eu faço isso via &lt;strong&gt;MCP (Model Context Protocol)&lt;/strong&gt;, que expõe as operações externas como ferramentas que o agente pode chamar.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"git-provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;pacote-mcp-do-provedor&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"--arg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"valor"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"PROVIDER_ORG_URL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;url-da-organizacao&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"PROVIDER_DEFAULT_PROJECT"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;projeto&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"PROVIDER_DEFAULT_REPOSITORY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;repo&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O MCP é o que conecta o "cérebro" (o agente) às "mãos" (as operações no provedor de git). E, combinado com a &lt;code&gt;allowlist&lt;/code&gt; lá do agente, eu controlo exatamente quais dessas operações ficam disponíveis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Camada 6 — Docs de onboarding
&lt;/h2&gt;

&lt;p&gt;Por último, mantenho um doc curto por agente explicando &lt;em&gt;o que ele é&lt;/em&gt; e &lt;em&gt;como invocar&lt;/em&gt;. É o que eu passo pra qualquer pessoa nova do time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Agente `open-pr`&lt;/span&gt;

&lt;span class="gu"&gt;## Como invocar&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Atalho: Ctrl+Shift+P
&lt;span class="p"&gt;-&lt;/span&gt; Ou: "Abre um PR para a demanda 123" / "Commita e abre PR"

&lt;span class="gu"&gt;## Fluxo&lt;/span&gt;
Usuário informa demanda → busca contexto → cria branch →
commit → push → abre PR → linka work item → retorna o link

&lt;span class="gu"&gt;## Onde vivem as regras&lt;/span&gt;
| Assunto              | Arquivo                     |
|----------------------|-----------------------------|
| Convenção de branch  | skills/create-branch        |
| Convenção de commit  | skills/commit               |
| Regras de PR         | skills/open-pr              |
| Guardrails           | steering/open-pr            |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  O resultado na prática
&lt;/h2&gt;

&lt;p&gt;Depois que esse setup entrou no fluxo, abrir um PR virou uma frase: &lt;em&gt;"abre um PR pra demanda X"&lt;/em&gt;. O agente infere a branch, commita no padrão, abre o PR com título e descrição corretos e linka o work item — sempre do mesmo jeito, sem eu precisar lembrar de cada detalhe.&lt;/p&gt;

&lt;p&gt;E, olhando o histórico de commits do repositório, dá pra ver a convenção sendo seguida de forma consistente: tipo, escopo, emoji e descrição curta em inglês, commit após commit. Essa consistência é exatamente o que eu queria — e ela não depende mais da minha memória num dia cansado.&lt;/p&gt;

&lt;h2&gt;
  
  
  O que eu levaria pra qualquer projeto
&lt;/h2&gt;

&lt;p&gt;Se eu fosse resumir o aprendizado em princípios reaproveitáveis:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Separe configuração de conhecimento.&lt;/strong&gt; O agente diz &lt;em&gt;o que pode&lt;/em&gt;; a skill diz &lt;em&gt;como fazer&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guardrails são uma camada à parte.&lt;/strong&gt; Proibições não deveriam estar misturadas com instruções normais.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skills pequenas e reutilizáveis.&lt;/strong&gt; Um agente complexo é composição de skills simples.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allowlist de ferramentas sempre.&lt;/strong&gt; Segurança por design, não por confiança.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diga o que NÃO fazer.&lt;/strong&gt; Isso reduz comportamento imprevisível mais do que qualquer instrução positiva.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mantenha dados sensíveis fora das skills.&lt;/strong&gt; Injete por configuração/MCP, nunca no texto versionado.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No fim, não automatizei só tarefas — padronizei &lt;em&gt;decisões&lt;/em&gt;. E isso, mais do que o tempo economizado, foi o que mudou meu fluxo de trabalho.&lt;/p&gt;

&lt;p&gt;Se você também vive repetindo os mesmos passos de Git, vale experimentar esse padrão. Comece com uma skill só, como a de commit, e vá compondo a partir daí.&lt;/p&gt;




&lt;p&gt;🇺🇸 English version: &lt;a href="https://dev.to/indiorlei/how-i-turned-repetitive-git-chores-into-ai-agents-with-the-kiro-cli-19h4"&gt;How I turned repetitive Git chores into AI agents with the Kiro CLI&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>git</category>
      <category>automation</category>
    </item>
    <item>
      <title>How I turned repetitive Git chores into AI agents with the Kiro CLI</title>
      <dc:creator>Indiorlei de Oliveira</dc:creator>
      <pubDate>Sun, 11 Oct 2026 13:41:02 +0000</pubDate>
      <link>https://dev.to/indiorlei/how-i-turned-repetitive-git-chores-into-ai-agents-with-the-kiro-cli-19h4</link>
      <guid>https://dev.to/indiorlei/how-i-turned-repetitive-git-chores-into-ai-agents-with-the-kiro-cli-19h4</guid>
      <description>&lt;p&gt;Every team has that set of boring tasks nobody enjoys but everybody has to do the exact same way every time: open a PR with the right title format, name the branch correctly, build the changelog, link the work item. These are low-intellectual-value, high-attention-cost chores — and worse, any deviation from the standard becomes noise during review.&lt;/p&gt;

&lt;p&gt;I decided to tackle this differently: instead of writing yet another giant bash script, I built &lt;strong&gt;specialized AI agents&lt;/strong&gt; that encapsulate these workflows. In this article I'll show how I structured that setup using the &lt;strong&gt;Kiro CLI&lt;/strong&gt;, with a pattern that cleanly separates &lt;em&gt;configuration&lt;/em&gt;, &lt;em&gt;knowledge&lt;/em&gt;, and &lt;em&gt;safety rules&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;I'll use my own agents as the example — with internal names abstracted away — but the pattern works for any team.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;In my day-to-day, I repeated two workflows several times a day:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Opening a Pull Request&lt;/strong&gt;: create the branch in the right format, commit with the correct convention, infer the target branch, open the PR with a standardized title and description, and link the work item.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Closing a release package&lt;/strong&gt;: find the already-completed PRs for a work item, generate the changelog, commit it, and open a release PR against the right branch.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these steps is hard. The problem is that every one of them has &lt;em&gt;details&lt;/em&gt; — and a forgotten detail means rework. Automating with a pure script would be fragile, because many of the decisions depend on context (which branch, which title, which target).&lt;/p&gt;

&lt;p&gt;AI agents are great precisely in that gray zone: they follow rules, but they interpret context.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea: separate configuration, knowledge, and rules
&lt;/h2&gt;

&lt;p&gt;What helped me the most was &lt;em&gt;not&lt;/em&gt; throwing everything into one giant prompt. Instead, I split each agent into four layers, each in its own file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.kiro/
├── agents/        # Configuration: which tools the agent is allowed to use
├── prompts/       # Identity: who the agent is and its fixed context
├── steering/      # Guardrails: what it must NEVER do
├── skills/        # Knowledge: the step-by-step for each task
└── settings/      # Integration with external tools (via MCP)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight is that each layer has a single responsibility:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agent&lt;/strong&gt; = the contract (allowed tools, shortcut, welcome message)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt&lt;/strong&gt; = the identity (context that never changes)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Steering&lt;/strong&gt; = the limits (non-negotiable prohibitions)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skill&lt;/strong&gt; = the manual (how to execute each step, with examples)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Splitting it this way brings a huge benefit: &lt;strong&gt;skills become reusable across agents.&lt;/strong&gt; The "create commit" skill is the same for the agent that opens PRs and the one that closes releases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1 — The agent (configuration)
&lt;/h2&gt;

&lt;p&gt;The agent's config file is lean. It states &lt;strong&gt;what&lt;/strong&gt; the agent can do, not &lt;strong&gt;how&lt;/strong&gt;. Here's an example (abstracted):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"open-pr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Agent that opens Pull Requests: creates a branch, commits, opens the PR and links the work item."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"file://./prompts/open-pr.md"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"includeMcpJson"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"shell"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"grep"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"glob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@git-provider"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"allowedTools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"shell"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"@git-provider/create_pull_request"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"@git-provider/create_branch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"@git-provider/link_work_item"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resources"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"skill://.kiro/skills/commit/SKILL.md"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"skill://.kiro/skills/create-branch/SKILL.md"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"skill://.kiro/skills/open-pr/SKILL.md"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"keyboardShortcut"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ctrl+shift+p"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"welcomeMessage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Give me the work item ID or describe the feature. I'll handle the rest."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two important points here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;allowedTools&lt;/code&gt; is an explicit allowlist.&lt;/strong&gt; The agent can only call exactly the tools I listed. That's security by design: it won't accidentally call a destructive tool I never authorized.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;resources&lt;/code&gt; injects the skills.&lt;/strong&gt; The agent loads detailed instructions on demand instead of carrying everything all the time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Layer 2 — The prompt (identity and fixed context)
&lt;/h2&gt;

&lt;p&gt;The prompt defines who the agent is and which context never changes. This is where I "anchor" the agent so it doesn't keep asking the obvious every time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;You are an agent specialized in opening Pull Requests for the
team's repository.

&lt;span class="gu"&gt;## Fixed Context&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Git provider: &lt;span class="nt"&gt;&amp;lt;configured&lt;/span&gt; &lt;span class="na"&gt;via&lt;/span&gt; &lt;span class="na"&gt;MCP&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Project / repository: &lt;span class="nt"&gt;&amp;lt;fixed&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;injected&lt;/span&gt; &lt;span class="na"&gt;by&lt;/span&gt; &lt;span class="na"&gt;configuration&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Default source branch: &lt;span class="sb"&gt;`main`&lt;/span&gt;

&lt;span class="gu"&gt;## Operating Rules&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; ALWAYS use the fixed project and repository — NEVER ask the user
&lt;span class="p"&gt;-&lt;/span&gt; Infer the target branch from the work item context
&lt;span class="p"&gt;-&lt;/span&gt; Follow the branch and commit conventions defined in the skills

&lt;span class="gu"&gt;## What NOT to do&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Do NOT ask for project or repository — they are fixed
&lt;span class="p"&gt;-&lt;/span&gt; Do NOT push directly to protected branches
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the &lt;strong&gt;"Rules" + "What NOT to do"&lt;/strong&gt; pattern. Telling the agent what &lt;em&gt;not&lt;/em&gt; to do is just as important as telling it what to do. It dramatically reduces unpredictable behavior.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Tip: data that must not leak (internal IDs, GUIDs, URLs) stays out of public articles/docs and is injected via configuration or MCP. Never put that in the body of a skill that lands in an open repository.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Layer 3 — Steering (non-negotiable guardrails)
&lt;/h2&gt;

&lt;p&gt;This is the layer that lets me sleep at night. Steering lists &lt;strong&gt;absolute prohibitions&lt;/strong&gt;, independent of any instruction the agent receives.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Open PR — Guardrails&lt;/span&gt;

&lt;span class="gu"&gt;## Prohibitions&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; NEVER force push
&lt;span class="p"&gt;-&lt;/span&gt; NEVER push directly to protected branches
&lt;span class="p"&gt;-&lt;/span&gt; NEVER create a PR without at least 1 commit on the branch
&lt;span class="p"&gt;-&lt;/span&gt; NEVER proceed if git shows unresolved conflicts

&lt;span class="gu"&gt;## Mandatory Confirmations&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; ALWAYS show the summary (branch, commits, target, title) before opening the PR
&lt;span class="p"&gt;-&lt;/span&gt; ALWAYS confirm if there's any doubt about the target branch

&lt;span class="gu"&gt;## Failure Behavior&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; If the push fails → show the error and suggest an action (never force)
&lt;span class="p"&gt;-&lt;/span&gt; If the target branch doesn't exist → ask the user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The distinction between &lt;em&gt;prompt&lt;/em&gt; and &lt;em&gt;steering&lt;/em&gt; is intentional: the prompt is the personality (it can have nuance), the steering is the law (no exceptions). Even if the user says "just force push it," the guardrail holds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 4 — Skills (reusable knowledge)
&lt;/h2&gt;

&lt;p&gt;Skills are the heart of the setup. Each one is a short document that teaches &lt;strong&gt;one&lt;/strong&gt; procedure, with a format and examples. The commit skill, for instance, defines the full convention:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Responsibility&lt;/span&gt;
Create standardized commits.

&lt;span class="gu"&gt;## Format&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;type&amp;gt;&lt;/span&gt;(&lt;span class="nt"&gt;&amp;lt;scope&amp;gt;&lt;/span&gt;): &lt;span class="nt"&gt;&amp;lt;emoji&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;short&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="gu"&gt;## Procedure&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Run &lt;span class="sb"&gt;`git status --short`&lt;/span&gt;
&lt;span class="p"&gt;2.&lt;/span&gt; Analyze the paths to determine type and scope
&lt;span class="p"&gt;3.&lt;/span&gt; Group into logical commits
&lt;span class="p"&gt;4.&lt;/span&gt; Stage selectively (never &lt;span class="sb"&gt;`git add .`&lt;/span&gt;)
&lt;span class="p"&gt;5.&lt;/span&gt; Commit with the formatted message

&lt;span class="gu"&gt;## Examples&lt;/span&gt;
feat(module-a): ✨ add product tag component
fix(module-b): 🐛 correct price display
refactor(shared): 🔨 simplify item props
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The strong point: &lt;strong&gt;the same commit skill is used by both the PR agent and the release agent.&lt;/strong&gt; I wrote it once and reused it everywhere. When the convention changes, I edit a single file.&lt;/p&gt;

&lt;p&gt;My second agent — the release closer — is basically &lt;strong&gt;skill orchestration&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Receives the work item ID
  → finds the already-completed PRs
  → switches to the right branch
  → generates the changelog
  → uses the commit skill
  → uses the open-pr skill to open the release PR
  → links the work item
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In other words: a complex agent emerges from composing simple skills. That's far easier to maintain than a monolithic prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 5 — Integration via MCP
&lt;/h2&gt;

&lt;p&gt;For the agent to &lt;em&gt;actually&lt;/em&gt; open PRs and fetch work items, it needs to talk to the git provider. I do this via &lt;strong&gt;MCP (Model Context Protocol)&lt;/strong&gt;, which exposes external operations as tools the agent can call.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"git-provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;provider-mcp-package&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"--arg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"PROVIDER_ORG_URL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;org-url&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"PROVIDER_DEFAULT_PROJECT"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;project&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"PROVIDER_DEFAULT_REPOSITORY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;repo&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MCP is what connects the "brain" (the agent) to the "hands" (the operations on the git provider). And combined with the agent's &lt;code&gt;allowlist&lt;/code&gt;, I control exactly which of those operations are available.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 6 — Onboarding docs
&lt;/h2&gt;

&lt;p&gt;Finally, I keep a short doc per agent explaining &lt;em&gt;what it is&lt;/em&gt; and &lt;em&gt;how to invoke it&lt;/em&gt;. It's what I hand to anyone new on the team:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# `open-pr` Agent&lt;/span&gt;

&lt;span class="gu"&gt;## How to invoke&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Shortcut: Ctrl+Shift+P
&lt;span class="p"&gt;-&lt;/span&gt; Or: "Open a PR for work item 123" / "Commit and open a PR"

&lt;span class="gu"&gt;## Flow&lt;/span&gt;
User provides a work item → fetch context → create branch →
commit → push → open PR → link work item → return the link

&lt;span class="gu"&gt;## Where the rules live&lt;/span&gt;
| Topic               | File                        |
|---------------------|-----------------------------|
| Branch convention   | skills/create-branch        |
| Commit convention   | skills/commit               |
| PR rules            | skills/open-pr              |
| Guardrails          | steering/open-pr            |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The result in practice
&lt;/h2&gt;

&lt;p&gt;Once this setup entered my workflow, opening a PR became a single sentence: &lt;em&gt;"open a PR for work item X."&lt;/em&gt; The agent infers the branch, commits in the right format, opens the PR with the correct title and description, and links the work item — always the same way, without me having to remember each detail.&lt;/p&gt;

&lt;p&gt;And looking at the repository's commit history, you can see the convention being followed consistently: type, scope, emoji, and a short English description, commit after commit. That consistency is exactly what I wanted — and it no longer depends on my memory on a tired day.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd take to any project
&lt;/h2&gt;

&lt;p&gt;If I had to boil the lessons down into reusable principles:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Separate configuration from knowledge.&lt;/strong&gt; The agent says &lt;em&gt;what it can do&lt;/em&gt;; the skill says &lt;em&gt;how to do it&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guardrails are their own layer.&lt;/strong&gt; Prohibitions shouldn't be mixed with normal instructions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small, reusable skills.&lt;/strong&gt; A complex agent is a composition of simple skills.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always allowlist tools.&lt;/strong&gt; Security by design, not by trust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Say what NOT to do.&lt;/strong&gt; It reduces unpredictable behavior more than any positive instruction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep sensitive data out of skills.&lt;/strong&gt; Inject it via configuration/MCP, never in versioned text.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In the end, I didn't just automate tasks — I standardized &lt;em&gt;decisions&lt;/em&gt;. And that, more than the time saved, is what actually changed my workflow.&lt;/p&gt;

&lt;p&gt;If you also find yourself repeating the same Git steps over and over, this pattern is worth a try. Start with a single skill, like the commit one, and compose from there.&lt;/p&gt;




&lt;p&gt;🇧🇷 Versão em português: &lt;a href="https://dev.to/indiorlei/como-transformei-tarefas-repetitivas-de-git-em-agentes-de-ia-com-o-kiro-cli-3d8f"&gt;Como transformei tarefas repetitivas de Git em agentes de IA com o Kiro CLI&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>git</category>
      <category>automation</category>
    </item>
    <item>
      <title>Como criei um tema para VS Code e terminal como projeto de estudo</title>
      <dc:creator>Indiorlei de Oliveira</dc:creator>
      <pubDate>Wed, 09 Sep 2026 00:48:15 +0000</pubDate>
      <link>https://dev.to/indiorlei/como-criei-um-tema-para-vs-code-e-terminal-como-projeto-de-estudo-5c5n</link>
      <guid>https://dev.to/indiorlei/como-criei-um-tema-para-vs-code-e-terminal-como-projeto-de-estudo-5c5n</guid>
      <description>&lt;p&gt;Temas parecem um projeto pequeno: escolher algumas cores e trocar o arquivo de configuração do editor. Foi exatamente com essa expectativa que comecei o &lt;strong&gt;Underground&lt;/strong&gt;, um tema escuro para VS Code e Kiro IDE. No caminho, descobri que um tema bom não é uma coleção de cores bonitas; é um pequeno sistema de design aplicado a muitos contextos.&lt;/p&gt;

&lt;p&gt;O Underground nasceu como projeto de estudo. A proposta era simples: uma interface escura, discreta, com contraste confortável e um verde quente como ponto de atenção. Depois, o projeto cresceu para incluir o terminal integrado do editor, o Windows Terminal e o Terminal.app do macOS — todos orientados pela mesma paleta.&lt;/p&gt;

&lt;p&gt;Neste artigo, compartilho o processo, as decisões e os problemas que encontrei. O repositório está disponível em &lt;a href="https://github.com/indiorlei/underground-theme" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comecei pela intenção, não pelas cores
&lt;/h2&gt;

&lt;p&gt;Antes de escrever JSON, defini a sensação que queria criar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fundo escuro sem ser preto absoluto;&lt;/li&gt;
&lt;li&gt;pouca competição visual entre as áreas da interface;&lt;/li&gt;
&lt;li&gt;cores de sintaxe que ajudem a escanear o código;&lt;/li&gt;
&lt;li&gt;cores semânticas previsíveis para erro, aviso, Git e conflitos;&lt;/li&gt;
&lt;li&gt;terminal e editor com a mesma identidade.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Essa primeira etapa evitou uma armadilha comum: atribuir cores isoladamente à medida que os elementos aparecem. Em vez disso, cada escolha deveria responder a uma função. O verde, por exemplo, não existe apenas porque eu gosto dele; ele marca elementos ativos e também palavras-chave, parâmetros e tags. Ele é uma cor de atenção moderada, não uma cor usada em todo lugar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Uma paleta pequena, com papéis claros
&lt;/h2&gt;

&lt;p&gt;O núcleo do tema é composto por poucos tons:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Papel&lt;/th&gt;
&lt;th&gt;Cor&lt;/th&gt;
&lt;th&gt;Uso principal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fundo&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#222222&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;editor, barra lateral e painéis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Superfície elevada&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#272727&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;abas ativas, inputs e status bar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Texto principal&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#ffffffde&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;conteúdo de maior prioridade&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Texto secundário&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#ffffff99&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;descrições e informações auxiliares&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Acento verde&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#9bd4b2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;atividade, parâmetros e Git modificado&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azul&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#90aed3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;strings, funções e números&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Roxo&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#ceb0d3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;tipos, interfaces e atributos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comentários&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#8a8a8a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;conteúdo propositalmente secundário&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Um detalhe importante é o &lt;code&gt;de&lt;/code&gt; em &lt;code&gt;#ffffffde&lt;/code&gt;. Em temas do VS Code, também podemos usar hex com canal alfa. Assim, em vez de inventar muitos cinzas diferentes, usei branco com transparências distintas para criar uma escala de hierarquia: texto principal, secundário e sutil.&lt;/p&gt;

&lt;p&gt;Essa abordagem deixou as decisões mais fáceis de manter. Quando percebi que comentários estavam apagados demais, por exemplo, não precisei redesenhar o tema inteiro: subi seu tom de &lt;code&gt;#6a6a6a&lt;/code&gt; para &lt;code&gt;#8a8a8a&lt;/code&gt;, preservando seu papel visual e melhorando a legibilidade.&lt;/p&gt;

&lt;h2&gt;
  
  
  A estrutura mínima de uma extensão de tema
&lt;/h2&gt;

&lt;p&gt;Uma extensão de tema do VS Code pode ser surpreendentemente pequena. No caso do Underground, os dois arquivos essenciais são:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;editors/vscode/
├── package.json
└── themes/
    └── underground-color-theme.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O &lt;code&gt;package.json&lt;/code&gt; registra o tema na seção &lt;code&gt;contributes&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"contributes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"themes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Underground"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"uiTheme"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vs-dark"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./themes/underground-color-theme.json"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O arquivo de tema contém duas partes que vale separar mentalmente:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;colors&lt;/code&gt;: a interface do VS Code — editor, abas, listas, widgets, terminal e estado do Git.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tokenColors&lt;/code&gt; e &lt;code&gt;semanticTokenColors&lt;/code&gt;: a aparência do código.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Essa separação parece óbvia, mas foi uma boa forma de organizar o trabalho. Primeiro deixei o ambiente confortável para navegar; depois cuidei da leitura do código.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tokens de sintaxe: cor é uma ferramenta de leitura
&lt;/h2&gt;

&lt;p&gt;Para a primeira camada de sintaxe, associei grupos de scopes a papéis visuais. Uma versão reduzida da configuração é esta:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"keyword"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"storage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"variable.parameter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"entity.name.tag"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"settings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"foreground"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#9bd4b2"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"support.function"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"constant.numeric"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"settings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"foreground"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#90aed3"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"entity.name.type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"entity.other.attribute-name"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"settings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"foreground"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#ceb0d3"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O ponto não é decorar scopes, e sim testar em linguagens diferentes. A mesma categoria pode receber scopes diferentes em TypeScript, HTML, JSON ou Markdown. Por isso mantive uma amostra de código para abrir no editor enquanto ajustava o tema.&lt;/p&gt;

&lt;p&gt;Também adicionei &lt;code&gt;semanticTokenColors&lt;/code&gt;. Essa camada moderna permite ao editor informar significados como &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;function&lt;/code&gt;, &lt;code&gt;parameter&lt;/code&gt; e &lt;code&gt;variable.readonly&lt;/code&gt;, especialmente útil em TypeScript e JavaScript. O resultado é mais consistente do que depender exclusivamente da gramática textual.&lt;/p&gt;

&lt;h2&gt;
  
  
  A parte que quase sempre fica esquecida: a interface inteira
&lt;/h2&gt;

&lt;p&gt;Colorir strings e palavras-chave é só o começo. Um tema realmente utilizável precisa lidar com estados menos frequentes: busca, sugestões, mensagens de validação, diff, merge conflict, Peek Definition, foco por teclado e decoração de Git.&lt;/p&gt;

&lt;p&gt;Esses casos revelam problemas que uma captura de tela comum não mostra. No Underground, encontrei dois exemplos marcantes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;os números de linha tinham contraste insuficiente e praticamente desapareciam;&lt;/li&gt;
&lt;li&gt;o widget de Peek Definition não tinha cores próprias e usava um fundo claro inesperado.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Corrigi o primeiro caso elevando o tom dos números inativos e mantendo o número ativo em branco. Para o segundo, foi preciso definir as chaves &lt;code&gt;peekView*&lt;/code&gt;, usando o fundo elevado e a seleção já existentes na paleta.&lt;/p&gt;

&lt;p&gt;Essa experiência mudou meu critério de pronto: um tema não está pronto quando o arquivo de exemplo está bonito. Ele está pronto depois de passar por fluxos reais de edição, busca, autocomplete, Git e resolução de conflitos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diferenças e conflitos também comunicam significado
&lt;/h2&gt;

&lt;p&gt;Para diffs, evitei blocos muito saturados. Linhas adicionadas usam verde com baixa opacidade; removidas usam vermelho com baixa opacidade. As palavras alteradas recebem uma opacidade um pouco maior que a linha inteira, para criar hierarquia.&lt;/p&gt;

&lt;p&gt;Nos conflitos de merge, mantive uma convenção semântica:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;versão atual: verde;&lt;/li&gt;
&lt;li&gt;versão recebida: azul;&lt;/li&gt;
&lt;li&gt;base comum: cinza;&lt;/li&gt;
&lt;li&gt;conflito ainda não resolvido: borda amarela.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;É uma decisão pequena, mas útil: as cores deixam de ser decoração e passam a explicar o estado da informação.&lt;/p&gt;

&lt;h2&gt;
  
  
  Um tema, mais de um ambiente
&lt;/h2&gt;

&lt;p&gt;Eu queria que abrir o terminal não parecesse mudar de aplicação. Por isso, documentei a paleta em &lt;code&gt;PALETTE.md&lt;/code&gt; e usei o tema do VS Code como fonte canônica. A partir dos valores &lt;code&gt;terminal.ansi*&lt;/code&gt;, criei:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;um esquema JSON para o Windows Terminal;&lt;/li&gt;
&lt;li&gt;um perfil &lt;code&gt;.terminal&lt;/code&gt; para o Terminal.app do macOS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Há uma sutileza aqui: o fundo do editor é &lt;code&gt;#222222&lt;/code&gt;, mas o preto ANSI é &lt;code&gt;#1a1a1a&lt;/code&gt;. Isso preserva a convenção de cores do terminal sem transformar toda a interface em preto absoluto.&lt;/p&gt;

&lt;p&gt;Ter uma fonte de verdade reduz divergências. Se eu alterar o amarelo de aviso, sei que preciso revisar o amarelo ANSI e a documentação da paleta. Esse cuidado é mais simples do que tentar descobrir, meses depois, por que o editor e o terminal usam cores quase iguais.&lt;/p&gt;

&lt;h2&gt;
  
  
  O que eu faria desde o início em um próximo tema
&lt;/h2&gt;

&lt;p&gt;Se fosse começar novamente, eu adotaria estes hábitos desde o primeiro commit:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Criar uma paleta com nomes de papel, não apenas uma lista de hexadecimais.&lt;/li&gt;
&lt;li&gt;Preparar arquivos de exemplo com várias linguagens e estados de interface.&lt;/li&gt;
&lt;li&gt;Testar acessibilidade e contraste antes de considerar uma cor definitiva.&lt;/li&gt;
&lt;li&gt;Documentar a relação entre editor e terminal desde cedo.&lt;/li&gt;
&lt;li&gt;Registrar mudanças visuais no changelog, incluindo o motivo da alteração.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Também vale testar o tema localmente por meio de um link simbólico na pasta de extensões do VS Code. Esse ciclo curto — editar, recarregar a janela, observar — tornou os ajustes muito mais rápidos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusão
&lt;/h2&gt;

&lt;p&gt;Criar o Underground foi uma forma excelente de estudar ferramentas de desenvolvimento por outro ângulo. Um tema não altera a lógica do programa, mas influencia diretamente a experiência de ler, navegar e depurar código por horas.&lt;/p&gt;

&lt;p&gt;A maior lição foi esta: consistência é mais valiosa do que quantidade de cores. Quando fundo, contraste, estados e semântica contam a mesma história, o resultado é mais calmo e mais útil.&lt;/p&gt;

&lt;p&gt;Se você quiser explorar a implementação, contribuir ou adaptar a paleta, o código está no &lt;a href="https://github.com/indiorlei/underground-theme" rel="noopener noreferrer"&gt;repositório do Underground&lt;/a&gt;, sob licença MIT.&lt;/p&gt;

</description>
      <category>vscode</category>
      <category>devtools</category>
      <category>opensource</category>
      <category>produtividade</category>
    </item>
  </channel>
</rss>
