<?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: Fernando Paladini</title>
    <description>The latest articles on DEV Community by Fernando Paladini (@paladini).</description>
    <link>https://dev.to/paladini</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%2F1065831%2F79b4d650-5838-4481-a62f-8f03f4010512.jpeg</url>
      <title>DEV Community: Fernando Paladini</title>
      <link>https://dev.to/paladini</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/paladini"/>
    <language>en</language>
    <item>
      <title>Run Local Audio Transcription with EchoTranscribe and Whisper</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Mon, 03 Aug 2026 12:35:57 +0000</pubDate>
      <link>https://dev.to/paladini/run-local-audio-transcription-with-echotranscribe-and-whisper-2hii</link>
      <guid>https://dev.to/paladini/run-local-audio-transcription-with-echotranscribe-and-whisper-2hii</guid>
      <description>&lt;p&gt;Sending a recording to a hosted transcription service is convenient, but it also creates a data-handling decision. Meeting audio, interviews, research notes, and draft content may be easier to process when the files stay on the computer that owns them.&lt;/p&gt;

&lt;p&gt;This tutorial walks through the documented development path for &lt;a href="https://github.com/paladini/echo-transcribe" rel="noopener noreferrer"&gt;EchoTranscribe&lt;/a&gt;, an MIT-licensed desktop application by Fernando Paladini. It combines a Tauri desktop shell, a React and TypeScript frontend, and a local FastAPI backend that loads Whisper models through faster-whisper.&lt;/p&gt;

&lt;p&gt;The goal is not to claim that local transcription is automatically more accurate or faster. The useful outcome is a reproducible local workflow: start the backend, open the desktop app, choose a model, transcribe supported audio, inspect timestamps, and export TXT, SRT, or JSON.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Use the stable &lt;code&gt;v0.1.1&lt;/code&gt; release, install the frontend dependencies, start the backend, and then run the Tauri development app in a second terminal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--branch&lt;/span&gt; v0.1.1 https://github.com/paladini/echo-transcribe.git
&lt;span class="nb"&gt;cd &lt;/span&gt;echo-transcribe
npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the first terminal, start the Python backend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;src-tauri/backend
python main.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the second terminal, from the repository root, start Tauri:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run tauri dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The backend documents its API at &lt;a href="http://localhost:8000/docs" rel="noopener noreferrer"&gt;http://localhost:8000/docs&lt;/a&gt;, and the Tauri window should open the frontend automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;v0.1.1&lt;/code&gt; README lists these prerequisites:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 18 or newer&lt;/li&gt;
&lt;li&gt;Python 3.8 or newer&lt;/li&gt;
&lt;li&gt;Rust for Tauri compilation&lt;/li&gt;
&lt;li&gt;Microsoft Visual Studio C++ Build Tools on Windows&lt;/li&gt;
&lt;li&gt;The Linux system packages documented in the README when developing on Ubuntu or Debian&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project also has macOS setup guidance using Homebrew. Tauri compilation adds platform-specific requirements, so a successful Python installation alone is not enough to build the desktop application.&lt;/p&gt;

&lt;p&gt;The backend requirements pin FastAPI 0.104.1, Uvicorn 0.24.0, faster-whisper 0.9.0, Pydantic 2.5.0, and supporting packages. PyTorch and torchaudio are specified as version 2.0.0 or newer. Read the repository's current dependency files before installing if you are working from &lt;code&gt;main&lt;/code&gt; instead of the stable tag.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start the local backend
&lt;/h2&gt;

&lt;p&gt;The README provides a startup script for each platform, but the manual path makes the process easier to inspect. From the repository root, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;src-tauri/backend
python main.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The backend creates model and temporary directories below &lt;code&gt;~/.echo-transcribe&lt;/code&gt;. It searches ports 8000 through 8004 and writes the selected port to &lt;code&gt;backend_port.txt&lt;/code&gt;. When port 8000 is available, the documented API address is &lt;code&gt;http://127.0.0.1:8000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Before launching the desktop shell, check the backend's health endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:8000/health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A healthy response includes &lt;code&gt;status&lt;/code&gt; set to &lt;code&gt;healthy&lt;/code&gt;. You can also open the generated OpenAPI documentation at &lt;a href="http://localhost:8000/docs" rel="noopener noreferrer"&gt;http://localhost:8000/docs&lt;/a&gt; to inspect the available routes.&lt;/p&gt;

&lt;p&gt;The first transcription may take longer because the selected Whisper model is downloaded when it is not already present. The source stores models under the &lt;code&gt;.echo-transcribe/models&lt;/code&gt; directory in your home folder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Launch the Tauri desktop app
&lt;/h2&gt;

&lt;p&gt;Keep the backend terminal running. Open another terminal at the repository root and run the exact command documented for Tauri development:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run tauri dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Tauri configuration uses &lt;code&gt;http://localhost:1420&lt;/code&gt; as the development URL and starts the Vite frontend with &lt;code&gt;npm run dev&lt;/code&gt;. The desktop window is configured with a 1200 by 800 initial size and can be resized.&lt;/p&gt;

&lt;p&gt;The application is not a remote client. Its frontend is paired with the backend running on your machine, and the backend's CORS configuration allows the local Vite origin and &lt;code&gt;tauri://localhost&lt;/code&gt;. That local arrangement is part of the privacy boundary, but it is not authentication.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transcribe a file
&lt;/h2&gt;

&lt;p&gt;In the app, select one audio file or a batch. The README documents MP3, WAV, FLAC, M4A, OGG, and WebM input, with a maximum of 10 files selected at once for batch transcription.&lt;/p&gt;

&lt;p&gt;Choose one of the models exposed by the backend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;tiny&lt;/code&gt;: 39 MB, faster with lower expected accuracy&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;base&lt;/code&gt;: 74 MB, a balance between speed and precision&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;small&lt;/code&gt;: 244 MB, better quality with medium speed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;medium&lt;/code&gt;: 769 MB, higher quality with slower processing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These sizes are the values in the tagged source's model metadata, not a benchmark for your machine. Runtime depends on the audio, model, dependency versions, and whether the environment can use an available accelerator.&lt;/p&gt;

&lt;p&gt;Leave automatic language detection enabled for a first test, or choose a language manually. The backend first validates the extension and model name. It then writes the upload to a temporary file, loads the selected Whisper model, transcribes with word timestamps enabled, and schedules cleanup of the temporary file.&lt;/p&gt;

&lt;p&gt;The result can be reviewed in the interface and exported as TXT, SRT, or JSON. Word-level timestamps are included in the backend response when the transcription model returns them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the path without transcribing a large recording
&lt;/h2&gt;

&lt;p&gt;Use a short audio file that you are allowed to process. Verify the workflow in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Confirm that &lt;code&gt;/health&lt;/code&gt; returns a healthy status.&lt;/li&gt;
&lt;li&gt;Open &lt;code&gt;/models&lt;/code&gt; and inspect the four model entries.&lt;/li&gt;
&lt;li&gt;Start with the &lt;code&gt;tiny&lt;/code&gt; or &lt;code&gt;base&lt;/code&gt; model to reduce the initial download and wait time.&lt;/li&gt;
&lt;li&gt;Transcribe a short file in one of the documented formats.&lt;/li&gt;
&lt;li&gt;Confirm that text and timestamps appear in the result.&lt;/li&gt;
&lt;li&gt;Export one result as TXT, SRT, or JSON and open the exported file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The backend also exposes &lt;code&gt;POST /transcribe&lt;/code&gt; and &lt;code&gt;POST /transcribe-batch&lt;/code&gt;. The interactive Swagger page at &lt;code&gt;/docs&lt;/code&gt; is the safest place to inspect the current multipart field names instead of guessing at a curl command. The single-file route accepts an uploaded &lt;code&gt;file&lt;/code&gt;, a &lt;code&gt;model&lt;/code&gt;, an optional &lt;code&gt;language&lt;/code&gt;, and the &lt;code&gt;auto_detect_language&lt;/code&gt; flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the local architecture works
&lt;/h2&gt;

&lt;p&gt;The project separates three responsibilities:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Tauri packages the desktop experience and connects the frontend to the local application.&lt;/li&gt;
&lt;li&gt;React, TypeScript, and Vite provide the interface and development server.&lt;/li&gt;
&lt;li&gt;FastAPI handles uploads, model loading, transcription, language detection, timestamps, and cleanup.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This separation keeps model execution out of the browser UI. It also makes the backend inspectable through OpenAPI while keeping the default network path on loopback. The model cache avoids downloading the same model for every request, and the temporary directory gives the backend a controlled place to write uploaded audio during processing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes and security boundaries
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The desktop window cannot load the backend.&lt;/strong&gt; Check that the Python process is still running and that port 8000 is available. If the backend selected another port, inspect &lt;code&gt;backend_port.txt&lt;/code&gt; and the terminal log.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The first request appears stuck.&lt;/strong&gt; Model loading and download happen before transcription. Check the network connection and backend logs. Do not use a long recording as the first test.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The file is rejected.&lt;/strong&gt; Confirm that its extension is one of MP3, WAV, FLAC, M4A, OGG, or WebM. The backend validates the suffix, not the contents of every possible container.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The build fails on a platform dependency.&lt;/strong&gt; Install the platform prerequisites listed by the README, including Rust and the required Linux packages or Windows C++ Build Tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You want to expose the API to another machine.&lt;/strong&gt; Stop and design that boundary first. The current backend allows broad methods and headers for its two local origins, and the repository does not document user authentication, quotas, or a production reverse proxy. Local execution limits where the default app sends data, but it does not turn an unauthenticated API into a safe public service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You process someone else's recording.&lt;/strong&gt; Local execution does not replace consent, retention, copyright, or organizational data-handling requirements. Only transcribe audio you are authorized to process.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does EchoTranscribe require a cloud API key?
&lt;/h3&gt;

&lt;p&gt;The documented backend loads Whisper models locally through faster-whisper and downloads missing models to the local model directory. The tutorial does not require a hosted transcription key.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use it without a GPU?
&lt;/h3&gt;

&lt;p&gt;The repository documents model choices and local execution, but it does not promise a particular hardware configuration or processing speed. Start with &lt;code&gt;tiny&lt;/code&gt; or &lt;code&gt;base&lt;/code&gt; and check the backend logs on your machine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is &lt;code&gt;main&lt;/code&gt; the same as the release?
&lt;/h3&gt;

&lt;p&gt;No. This tutorial uses the stable &lt;code&gt;v0.1.1&lt;/code&gt; tag. The repository's default branch can change, so pin the tag when you need the commands and model list described here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is it ready to expose as a shared service?
&lt;/h3&gt;

&lt;p&gt;That is outside the documented guarantee. Add authentication, request limits, storage controls, logging decisions, and a deliberate network boundary before considering shared access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;EchoTranscribe is a useful pattern for local AI desktop tooling: a Tauri shell, an inspectable FastAPI service, a persistent model directory, and explicit export formats. Pin &lt;code&gt;v0.1.1&lt;/code&gt;, verify the health endpoint, test with a short authorized recording, and treat the lack of authentication as a real deployment boundary.&lt;/p&gt;

&lt;p&gt;Which local transcription feature would you verify next: speaker separation, stronger export controls, or authenticated access for a small team?&lt;/p&gt;

&lt;h2&gt;
  
  
  AI assistance disclosure
&lt;/h2&gt;

&lt;p&gt;AI assistance was used to organize this tutorial and review its wording. The release, commands, versions, routes, model names, file paths, limitations, and security notes were checked against the public &lt;code&gt;v0.1.1&lt;/code&gt; repository sources linked above. No performance benchmark or personal usage claim is implied.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>tutorial</category>
      <category>whisper</category>
    </item>
    <item>
      <title>Self-Host Audio Stem Separation with Docker and Demucs</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Sun, 02 Aug 2026 12:36:20 +0000</pubDate>
      <link>https://dev.to/paladini/self-host-audio-stem-separation-with-docker-and-demucs-2hmk</link>
      <guid>https://dev.to/paladini/self-host-audio-stem-separation-with-docker-and-demucs-2hmk</guid>
      <description>&lt;p&gt;Turning a song into a vocal track, an instrumental, or separate drums and bass often leads to a choice between a hosted service and a complicated local machine setup. Hosted services can create privacy and upload concerns. A local setup can require Python, FFmpeg, PyTorch, model downloads, and a way to retrieve generated files.&lt;/p&gt;

&lt;p&gt;This tutorial uses &lt;a href="https://github.com/paladini/voice-separator-demucs" rel="noopener noreferrer"&gt;voice-separator-demucs&lt;/a&gt;, an MIT-licensed self-hosted application by Fernando Paladini. It provides a browser interface and a FastAPI backend for separating audio with Demucs. The Docker path keeps the model cache persistent and maps generated files to a directory on your computer.&lt;/p&gt;

&lt;p&gt;The result is a local service at &lt;code&gt;http://localhost:7860&lt;/code&gt;. You can upload an MP3, WAV, FLAC, M4A, or AAC file, choose stems such as vocals or drums, and download the generated MP3 files without sending the audio to a hosted application.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Clone the repository, start the Docker Compose service, open the local web interface, and upload an audio file. The current Compose file maps &lt;code&gt;./static/output&lt;/code&gt; to the container output directory and stores the model cache in a named Docker volume.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/paladini/voice-separator-demucs.git
&lt;span class="nb"&gt;cd &lt;/span&gt;voice-separator-demucs
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;a href="http://localhost:7860" rel="noopener noreferrer"&gt;http://localhost:7860&lt;/a&gt;. The first separation downloads the selected model, so the initial run takes longer than later runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker with the Compose plugin&lt;/li&gt;
&lt;li&gt;A machine with enough disk space for Python dependencies and the Demucs model cache&lt;/li&gt;
&lt;li&gt;An audio file you are allowed to process&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The repository also documents a Python path that needs Python 3.8 or newer and FFmpeg. Docker is the more reproducible starting point because the Dockerfile installs FFmpeg and uses Python 3.9 inside the image. The Dockerfile currently installs the unpinned dependencies from &lt;code&gt;requirements.txt&lt;/code&gt;, so this is a current-branch setup rather than a fully lockfile-reproducible build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start the local service
&lt;/h2&gt;

&lt;p&gt;Clone the repository and start the service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/paladini/voice-separator-demucs.git
&lt;span class="nb"&gt;cd &lt;/span&gt;voice-separator-demucs
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Compose configuration exposes port &lt;code&gt;7860&lt;/code&gt;, mounts &lt;code&gt;./static/output&lt;/code&gt; at &lt;code&gt;/app/static/output&lt;/code&gt;, and persists &lt;code&gt;/root/.cache&lt;/code&gt; in a named volume called &lt;code&gt;model-cache&lt;/code&gt;. The host output mount is useful because files created by the container remain available in the repository's &lt;code&gt;static/output&lt;/code&gt; directory.&lt;/p&gt;

&lt;p&gt;Check that the API is alive before uploading anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:7860/health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application defines this endpoint as a simple health check. A successful response has the following shape:&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="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"healthy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Voice Separator API is running"&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;You can also open the interactive FastAPI documentation at &lt;a href="http://localhost:7860/docs" rel="noopener noreferrer"&gt;http://localhost:7860/docs&lt;/a&gt;. The documented API includes &lt;code&gt;/api/stems&lt;/code&gt;, &lt;code&gt;/api/separate&lt;/code&gt;, and &lt;code&gt;/api/separate-youtube&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate vocals with the API
&lt;/h2&gt;

&lt;p&gt;The browser interface is the easiest path, but the API makes the workflow scriptable. The upload endpoint accepts a multipart file, a comma-separated &lt;code&gt;stems&lt;/code&gt; value, and an optional model name. The default model is &lt;code&gt;mdx_extra_q&lt;/code&gt; and the default stem is &lt;code&gt;vocals&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The following command uses the repository's current endpoint and parameter names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"http://localhost:7860/api/separate"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"file=@./example.mp3"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"stems=vocals"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On success, the response includes &lt;code&gt;success: true&lt;/code&gt;, the processed stems, an estimated processing time, and a file URL such as &lt;code&gt;/static/output/vocals_&amp;lt;id&amp;gt;.mp3&lt;/code&gt;. Because the output directory is mounted by Compose, the same file is also available under &lt;code&gt;static/output&lt;/code&gt; on the host.&lt;/p&gt;

&lt;p&gt;To request more than one stem, pass a comma-separated list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"http://localhost:7860/api/separate"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"file=@./example.wav"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"stems=vocals,instrumental"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The available selections in the current implementation are &lt;code&gt;drums&lt;/code&gt;, &lt;code&gt;bass&lt;/code&gt;, &lt;code&gt;other&lt;/code&gt;, &lt;code&gt;vocals&lt;/code&gt;, and &lt;code&gt;instrumental&lt;/code&gt;. The instrumental result combines drums, bass, and other. The backend validates both the model name and the selected stems before processing the file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understand the model choices
&lt;/h2&gt;

&lt;p&gt;The interface exposes four model names: &lt;code&gt;mdx_extra_q&lt;/code&gt;, &lt;code&gt;mdx&lt;/code&gt;, &lt;code&gt;htdemucs&lt;/code&gt;, and &lt;code&gt;htdemucs_ft&lt;/code&gt;. The application selects CPU for &lt;code&gt;mdx_extra_q&lt;/code&gt; and &lt;code&gt;mdx&lt;/code&gt; when a GPU is unavailable. The two &lt;code&gt;htdemucs&lt;/code&gt; variants require a CUDA-capable GPU in the current separator implementation.&lt;/p&gt;

&lt;p&gt;For a first local test, keep the default &lt;code&gt;mdx_extra_q&lt;/code&gt; model and request only vocals. The README describes it as the CPU-oriented default. It also notes that the first model download is about 200 MB, while exact disk usage and runtime depend on the dependency versions, hardware, audio length, and selected model.&lt;/p&gt;

&lt;p&gt;Do not treat the README's rough processing times as a benchmark. They are operational guidance, not a guarantee. A longer file, several selected stems, or CPU-only processing can take substantially longer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Compose mounts matter
&lt;/h2&gt;

&lt;p&gt;There are two different persistence concerns:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The named &lt;code&gt;model-cache&lt;/code&gt; volume prevents the model cache from disappearing when the container is recreated.&lt;/li&gt;
&lt;li&gt;The bind mount keeps generated audio in &lt;code&gt;./static/output&lt;/code&gt; on the host.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can inspect the container and output files with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="s2"&gt;"name=voice-separator"&lt;/span&gt;
Get-ChildItem .&lt;span class="se"&gt;\s&lt;/span&gt;tatic&lt;span class="se"&gt;\o&lt;/span&gt;utput
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second command is for PowerShell. On macOS or Linux, use &lt;code&gt;ls -lah static/output&lt;/code&gt; instead. If you use the single &lt;code&gt;docker run&lt;/code&gt; command from the README without a bind mount, output files stay inside the container and must be copied out with &lt;code&gt;docker cp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When you are done, stop the service without deleting the cache:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To remove the persistent model volume as well, use &lt;code&gt;docker compose down -v&lt;/code&gt;. That forces a future startup to download the model again.&lt;/p&gt;

&lt;h2&gt;
  
  
  YouTube input and its boundary
&lt;/h2&gt;

&lt;p&gt;The application also exposes &lt;code&gt;/api/separate-youtube&lt;/code&gt; and uses &lt;code&gt;yt-dlp&lt;/code&gt; to download audio before separation. The current route validates a YouTube URL and rejects videos longer than 10 minutes. It then removes the temporary downloaded audio after processing.&lt;/p&gt;

&lt;p&gt;This feature does not remove copyright or platform obligations. Process only material you have permission to download and transform. A public URL is not automatically a license to copy its audio.&lt;/p&gt;

&lt;p&gt;For a privacy-focused local workflow, prefer direct file upload. Both routes write output into the local application's static directory, and the app does not provide authentication in the current implementation. Do not expose port 7860 to the public internet without adding an access-control and deployment boundary appropriate for your environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes to check first
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;FFmpeg errors:&lt;/strong&gt; The Docker image installs FFmpeg. If you use the Python path, install FFmpeg separately as described in the repository README and confirm that &lt;code&gt;ffmpeg -version&lt;/code&gt; works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slow first request:&lt;/strong&gt; Model loading and the initial model download happen before separation. Check the container logs with &lt;code&gt;docker compose logs -f&lt;/code&gt; and wait for the model to finish loading.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Out of memory:&lt;/strong&gt; Use a smaller input, select fewer stems, close competing workloads, or use the CPU-oriented default. GPU model choices are not a universal speed-up if the machine lacks the required GPU memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No files on the host:&lt;/strong&gt; Confirm that you started with &lt;code&gt;docker compose up -d&lt;/code&gt; from the repository directory and that &lt;code&gt;static/output&lt;/code&gt; is the directory mounted by the Compose file. A plain &lt;code&gt;docker run&lt;/code&gt; without &lt;code&gt;-v&lt;/code&gt; keeps output inside the container.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Browser security warnings:&lt;/strong&gt; The documented default is plain HTTP on localhost. The README includes an optional self-signed HTTPS example for local development. A self-signed certificate is not a production trust model, and the current application has no authentication layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducible verification
&lt;/h2&gt;

&lt;p&gt;After the health check, verify the core path in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;code&gt;/api/stems&lt;/code&gt; and confirm the available stem names.&lt;/li&gt;
&lt;li&gt;Upload a short audio file with &lt;code&gt;stems=vocals&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Confirm the response contains &lt;code&gt;success: true&lt;/code&gt; and a generated output URL.&lt;/li&gt;
&lt;li&gt;Confirm a new MP3 appears in &lt;code&gt;static/output&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Play the output and compare its duration with the input.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The repository includes unit tests for mono-to-stereo tensor normalization. You can run the lightweight test module in a Python environment with its available dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; unittest tests/test_audio_tensor_utils.py &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test suite does not prove that every model, codec, GPU, or long audio file works. It verifies a focused preprocessing behavior used by the separator.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does the audio leave my computer?
&lt;/h3&gt;

&lt;p&gt;The Docker workflow runs the application locally. If you use the YouTube route, the app downloads the requested source first. Local execution does not make the application safe to expose publicly, because the current API has no authentication.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I run it without a GPU?
&lt;/h3&gt;

&lt;p&gt;Yes, the current implementation selects CPU for &lt;code&gt;mdx_extra_q&lt;/code&gt; and &lt;code&gt;mdx&lt;/code&gt;. The &lt;code&gt;htdemucs&lt;/code&gt; variants require a CUDA GPU according to the source code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is this a production-ready hosted service?
&lt;/h3&gt;

&lt;p&gt;No claim like that is supported by the repository. Treat it as a local or controlled self-hosted application. Add authentication, resource limits, storage cleanup, and a deliberate reverse-proxy boundary before considering a shared deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;The useful pattern is not only the separation model. It is the local boundary around it: Docker packages the runtime, a named volume preserves expensive model downloads, and a host bind mount makes generated files easy to retrieve. Start with the default CPU-oriented model and a short file, verify the health and output paths, then decide whether your hardware and usage rights support larger jobs.&lt;/p&gt;

&lt;p&gt;Have you found a reliable way to add authentication and per-job storage limits to a local audio-processing API without making the setup too complex?&lt;/p&gt;

&lt;h2&gt;
  
  
  AI assistance disclosure
&lt;/h2&gt;

&lt;p&gt;AI assistance was used to organize this tutorial and review its wording. The commands, endpoint names, model names, file paths, limitations, and security notes were checked against the current public repository sources linked above. No performance benchmark or personal usage claim is implied.&lt;/p&gt;

</description>
      <category>python</category>
      <category>docker</category>
      <category>tutorial</category>
      <category>audio</category>
    </item>
    <item>
      <title>Style Active Admin 3 and 4 with One Rails Theme Gem</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Fri, 31 Jul 2026 12:37:32 +0000</pubDate>
      <link>https://dev.to/paladini/style-active-admin-3-and-4-with-one-rails-theme-gem-32g7</link>
      <guid>https://dev.to/paladini/style-active-admin-3-and-4-with-one-rails-theme-gem-32g7</guid>
      <description>&lt;p&gt;Active Admin gives Rails applications a productive admin surface, but its styling depends on which major version your application uses. Active Admin 3 expects a Sass and Sprockets workflow. Active Admin 4 uses Tailwind CSS v4 and a different source-to-build path. A theme that treats them as the same problem can leave you with a successful generator run and an unstyled page.&lt;/p&gt;

&lt;p&gt;This tutorial uses &lt;a href="https://github.com/paladini/activeadmin-claude-theme" rel="noopener noreferrer"&gt;activeadmin-claude-theme&lt;/a&gt;, an MIT-licensed Rails engine gem, to install the same warm visual language on both Active Admin branches. The current &lt;code&gt;0.2.0&lt;/code&gt; release includes a version-aware generator, Active Admin 3 support, and Active Admin 4 support. The theme is a community project inspired by Claude aesthetics. It is not affiliated with or endorsed by Anthropic.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Add the gem, run its installer, and then use the asset step that belongs to your Active Admin version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle add activeadmin-claude-theme
rails generate activeadmin_claude_theme:install

&lt;span class="c"&gt;# Active Admin 4 only&lt;/span&gt;
npm run build:css
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Active Admin 4, the generator keeps Tailwind source in &lt;code&gt;app/assets/tailwind/active_admin.css&lt;/code&gt; and builds the served file into &lt;code&gt;app/assets/builds/active_admin.css&lt;/code&gt;. For Active Admin 3, it writes the Sass entry point at &lt;code&gt;app/assets/stylesheets/active_admin.scss&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need an existing Rails application with Active Admin installed, Ruby 3.2 or newer, and Rails 7.2 or newer. The theme's release notes document support for Active Admin 3.2 through the 3.x line and Active Admin 4.0.0.beta22 or newer. Active Admin 4 also requires the Node-based Tailwind build used by its asset setup.&lt;/p&gt;

&lt;p&gt;The commands below assume you have already run Active Admin's installer. If you are starting from an empty Rails application, follow the &lt;a href="https://activeadmin.info/documentation.html" rel="noopener noreferrer"&gt;Active Admin installation guide&lt;/a&gt; first, then return here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install the theme
&lt;/h2&gt;

&lt;p&gt;Add the gem to your application's &lt;code&gt;Gemfile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"activeadmin-claude-theme"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install dependencies and run the generator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle &lt;span class="nb"&gt;install
&lt;/span&gt;rails generate activeadmin_claude_theme:install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generator detects the installed Active Admin major version. It does not ask you to choose a stylesheet manually, because the integration points are different.&lt;/p&gt;

&lt;h2&gt;
  
  
  Active Admin 4: build the Tailwind output
&lt;/h2&gt;

&lt;p&gt;Active Admin 4 uses Tailwind v4. The theme generator vendors its CSS into the application, imports it from the Tailwind source, and updates the build script when the standard setup is present.&lt;/p&gt;

&lt;p&gt;Run the CSS build after installation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build:css
bin/rails server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important distinction is source versus served output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/assets/tailwind/active_admin.css   # Tailwind source
app/assets/builds/active_admin.css     # compiled output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The source file is outside the Propshaft-served path. Serving the uncompiled source instead of the build output is a common reason for seeing an admin page with little or none of the expected styling.&lt;/p&gt;

&lt;p&gt;The generator also adds the theme gem's view directory to the Tailwind content configuration when &lt;code&gt;tailwind-active_admin.config.js&lt;/code&gt; exists. That lets Tailwind see the engine templates that contain utility classes. The implementation uses &lt;code&gt;bundle show activeadmin-claude-theme&lt;/code&gt; to locate the installed gem rather than asking you to hard-code a machine-specific path.&lt;/p&gt;

&lt;p&gt;Active Admin's own dark-mode toggle remains part of the AA4 setup. The theme remaps color scales and adds semantic variables while preserving the existing hooks instead of replacing the whole plugin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Active Admin 3: use the Sass entry point
&lt;/h2&gt;

&lt;p&gt;Active Admin 3 follows a Sprockets and Sass path. After running the same generator, check &lt;code&gt;app/assets/stylesheets/active_admin.scss&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s2"&gt;"activeadmin_claude_theme/aa3/base"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then make sure your application includes a Sass pipeline such as &lt;code&gt;sassc-rails&lt;/code&gt; or &lt;code&gt;dartsass-rails&lt;/code&gt;, and restart Rails:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle &lt;span class="nb"&gt;install
&lt;/span&gt;bin/rails server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AA3 integration replaces the legacy &lt;code&gt;active_admin/base&lt;/code&gt; import when it finds one. If the stylesheet has a different structure, the generator appends the theme import so you can review the result before committing it.&lt;/p&gt;

&lt;p&gt;There is an intentional feature difference here: Active Admin 3 receives the light theme only. Native dark mode is documented for the AA4 path because Active Admin 3 does not provide the same dark-mode mechanism.&lt;/p&gt;

&lt;h2&gt;
  
  
  Customize the palette without replacing the theme
&lt;/h2&gt;

&lt;p&gt;The theme exposes semantic tokens for the common colors. On Active Admin 4, override CSS variables after the theme import:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--claude-primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#d4845f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--claude-canvas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#fff8f0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.dark&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--claude-canvas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#121110&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Active Admin 3, set Sass variables before importing the base theme:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="nv"&gt;$claude-primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;#d4845f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$claude-canvas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;#fff8f0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s2"&gt;"activeadmin_claude_theme/aa3/base"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is safer than editing generated gem files. The repository's &lt;a href="https://github.com/paladini/activeadmin-claude-theme/blob/main/DESIGN.md" rel="noopener noreferrer"&gt;design token reference&lt;/a&gt; lists the available roles, including the accent, canvas, ink, and body colors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the installation
&lt;/h2&gt;

&lt;p&gt;Use a short checklist rather than relying only on a successful generator exit code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Confirm the gem is locked to the version you reviewed. For this tutorial, that is &lt;code&gt;0.2.0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;On AA4, confirm the Tailwind source exists under &lt;code&gt;app/assets/tailwind/&lt;/code&gt; and the compiled CSS exists under &lt;code&gt;app/assets/builds/&lt;/code&gt; after &lt;code&gt;npm run build:css&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;On AA3, confirm the Sass entry imports &lt;code&gt;activeadmin_claude_theme/aa3/base&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Open an Active Admin dashboard and an index page. Check the header, navigation, table, form, and login screen.&lt;/li&gt;
&lt;li&gt;On AA4, toggle dark mode and verify that the existing control still works.&lt;/li&gt;
&lt;li&gt;Change one semantic token, rebuild the relevant assets, and confirm the rendered color changes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a repository-level verification, the project documents separate dummy applications and integration commands for AA4 and AA3. Running those tests is useful when upgrading the theme or changing its overrides:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# AA4&lt;/span&gt;
&lt;span class="nb"&gt;cd test&lt;/span&gt;/dummy
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run build:css
ruby bin/rails db:setup db:seed
&lt;span class="nb"&gt;cd&lt;/span&gt; ../..
ruby &lt;span class="nt"&gt;-Itest&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt;/activeadmin_claude_theme_test.rb
ruby &lt;span class="nt"&gt;-Itest&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt;/integration/theme_integration_test.rb

&lt;span class="c"&gt;# AA3&lt;/span&gt;
&lt;span class="nv"&gt;BUNDLE_GEMFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gemfiles/activeadmin_3.gemfile bundle &lt;span class="nb"&gt;install
cd test&lt;/span&gt;/dummy_aa3
bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rails db:setup db:seed
&lt;span class="nb"&gt;cd&lt;/span&gt; ../..
&lt;span class="nv"&gt;DUMMY_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dummy_aa3 &lt;span class="nv"&gt;BUNDLE_GEMFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gemfiles/activeadmin_3.gemfile &lt;span class="se"&gt;\&lt;/span&gt;
  ruby &lt;span class="nt"&gt;-Itest&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt;/activeadmin_claude_theme_test.rb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why the version-aware generator matters
&lt;/h2&gt;

&lt;p&gt;The visual goal is shared, but the integration boundary is not. AA4 needs Tailwind source discovery, a build command, and view content paths. AA3 needs a Sass import and a Sprockets-compatible manifest. The generator makes that branch explicit in code through Active Admin version detection.&lt;/p&gt;

&lt;p&gt;It also handles a subtle AA4 failure mode: a legacy &lt;code&gt;app/assets/stylesheets/active_admin.css&lt;/code&gt; file can be moved to &lt;code&gt;app/assets/tailwind/active_admin.css&lt;/code&gt; before compilation. That keeps the source from being treated as the final served asset by Propshaft.&lt;/p&gt;

&lt;p&gt;The result is not magic CSS. It is a small Rails engine with two integration paths, shared design tokens, and limited view overrides. That makes the generated changes inspectable in your own application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes and security boundaries
&lt;/h2&gt;

&lt;p&gt;If &lt;code&gt;rails generate activeadmin_claude_theme:install&lt;/code&gt; reports an unsupported Active Admin version, check the resolved bundle. Active Admin 2.x is outside the documented support range. If AA4 looks unstyled, rebuild CSS and check the source and build locations before changing templates. If AA3 fails to compile, verify the Sass dependency and import path.&lt;/p&gt;

&lt;p&gt;The theme changes presentation. It does not provide authentication, authorization, CSRF protection, or a security review of your admin resources. Active Admin's authorization and Devise configuration remain application responsibilities. Keep the admin surface behind your existing authentication and authorization controls, and review generated template changes before deploying them.&lt;/p&gt;

&lt;p&gt;Also treat the name accurately: this is a community theme, not an Anthropic product. The visual inspiration is not a security or compatibility guarantee.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does one installation command support both Active Admin versions?
&lt;/h3&gt;

&lt;p&gt;Yes. The generator detects Active Admin 3 or 4 and applies the corresponding asset setup. The follow-up verification differs because AA4 needs a CSS build while AA3 uses Sass.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need Node.js for Active Admin 3?
&lt;/h3&gt;

&lt;p&gt;Not for the theme's documented AA3 path. AA3 uses Sass through Sprockets. Node.js is required for the AA4 Tailwind workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use the theme with Propshaft?
&lt;/h3&gt;

&lt;p&gt;Yes for the documented Active Admin 4 setup, provided Tailwind source stays under &lt;code&gt;app/assets/tailwind/&lt;/code&gt; and the compiled file is emitted under &lt;code&gt;app/assets/builds/&lt;/code&gt;. AA3 typically uses Sprockets instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I keep my own brand colors?
&lt;/h3&gt;

&lt;p&gt;Yes. Override the semantic CSS variables for AA4 or Sass variables for AA3, then verify the generated CSS in the browser. Use the design token reference to choose the right role instead of changing many selectors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;The useful lesson is not the palette. It is respecting the boundary between Active Admin 3's Sass pipeline and Active Admin 4's Tailwind pipeline. A version-aware installer can keep the setup small, but you still need to verify the actual source path, compiled asset, and rendered admin pages.&lt;/p&gt;

&lt;p&gt;Have you kept one Rails admin theme working across a major asset-pipeline change? I would be interested in which compatibility boundary caused the most maintenance in your application.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI assistance disclosure
&lt;/h2&gt;

&lt;p&gt;AI assistance was used to organize this tutorial and review its wording. The technical details and commands were checked against the project's current README, generator implementation, gemspec, changelog, FAQ, override map, and the Active Admin documentation linked above.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>tutorial</category>
      <category>activeadmin</category>
    </item>
    <item>
      <title>Trace AI Coding Changes to Requirements with Python and SARIF</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Thu, 30 Jul 2026 13:23:28 +0000</pubDate>
      <link>https://dev.to/paladini/trace-ai-coding-changes-to-requirements-with-python-and-sarif-1967</link>
      <guid>https://dev.to/paladini/trace-ai-coding-changes-to-requirements-with-python-and-sarif-1967</guid>
      <description>&lt;p&gt;AI-assisted code can look complete while quietly missing a requirement, an expected file, a required test, or an acceptance condition. A green-looking diff does not prove that the change answers the request that created it.&lt;/p&gt;

&lt;p&gt;This tutorial builds a small evidence check with &lt;a href="https://github.com/paladini/spectrace-ai-coding" rel="noopener noreferrer"&gt;SpecTrace for AI Coding&lt;/a&gt;. You will describe requirements in JSON, map implementation files and test results to those requirements, then generate a Markdown report plus machine-readable JSON and SARIF output for CI review.&lt;/p&gt;

&lt;p&gt;The useful boundary is deliberate: SpecTrace does not ask an LLM whether code is good. Its version 0.1.0 implementation uses the Python standard library to check whether the evidence map covers the requirements that reviewers defined.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you will build
&lt;/h2&gt;

&lt;p&gt;The example models a checkout audit trail. One requirement asks for allow and deny decisions to be recorded. The evidence map links that requirement to a recorder file, a passing test, and two acceptance criteria.&lt;/p&gt;

&lt;p&gt;The flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;requirements JSON + change map JSON
                    |
                    v
              SpecTrace verifier
              /        |        \
             v         v         v
       Markdown     JSON      SARIF for CI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If any required link is missing, the verifier reports a finding and returns a nonzero status. That makes the check suitable for a pull request gate, provided the change map itself is produced and reviewed as part of your workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need Python 3.10 or newer and a PowerShell, macOS, or Linux shell. The project is distributed under the &lt;a href="https://github.com/paladini/spectrace-ai-coding/blob/main/LICENSE" rel="noopener noreferrer"&gt;MIT license&lt;/a&gt; and version 0.1.0 currently uses only Python's standard library at runtime.&lt;/p&gt;

&lt;p&gt;Clone the repository and enter it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;clone&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https://github.com/paladini/spectrace-ai-coding.git&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;spectrace-ai-coding&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The commands below use the repository's current documented examples. They do not require an API key, model account, or network call after cloning.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Validate the evidence inputs
&lt;/h2&gt;

&lt;p&gt;SpecTrace keeps the requirement definition separate from the change map. The bundled spec contains requirement IDs, expected file patterns, acceptance labels, and required test names. The change map supplies the files, test statuses, and acceptance evidence.&lt;/p&gt;

&lt;p&gt;Run the input validation command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;PYTHONPATH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="bp"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;\src"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;spectrace_ai_coding&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;validate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--spec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;examples\checkout-audit-spec.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--change-map&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;examples\checkout-audit-change-map.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see a message stating that two requirements, three file links, and two test results were validated. This step checks the structure and cross-references before report generation. It does not claim that the underlying application is correct.&lt;/p&gt;

&lt;p&gt;The important design detail is the shared requirement ID. A file or test only contributes evidence when its &lt;code&gt;requirement_ids&lt;/code&gt; list includes the ID from the spec. An unknown ID is a cross-reference error rather than a silent mismatch.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Generate reviewer and CI artifacts
&lt;/h2&gt;

&lt;p&gt;Now run verification and request all three output formats:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;PYTHONPATH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="bp"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;\src"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;spectrace_ai_coding&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;verify&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--spec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;examples\checkout-audit-spec.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--change-map&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;examples\checkout-audit-change-map.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;build\spectrace-report.md&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--json-out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;build\spectrace-report.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--sarif-out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;build\spectrace.sarif.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Markdown report is for human review. It includes a summary, a trace matrix, and requirement details. The JSON report is convenient for scripts that need counts and finding objects. The SARIF document follows the &lt;a href="https://docs.oasis-open.org/sarif/sarif/v2.1.0/cs01/sarif-v2.1.0-cs01.html" rel="noopener noreferrer"&gt;SARIF 2.1.0 format&lt;/a&gt;, so a CI platform that understands SARIF can display findings alongside other analysis results.&lt;/p&gt;

&lt;p&gt;For the bundled example, verification prints that two requirements passed and returns status 0. Inspect the generated Markdown before treating that result as useful review evidence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-Content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;build\spectrace-report.md&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The report should identify the checkout audit requirement, its linked files, its required tests, and its acceptance evidence. It is more useful than a bare pass count because reviewers can see what the change map actually claims.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. See a failure instead of trusting a green result
&lt;/h2&gt;

&lt;p&gt;The verifier distinguishes missing evidence from passing evidence. For example, if a required test is absent from the change map, the requirement receives a &lt;code&gt;missing-required-test&lt;/code&gt; finding. If the test exists with a status other than &lt;code&gt;passed&lt;/code&gt;, it receives a &lt;code&gt;required-test-not-passed&lt;/code&gt; finding.&lt;/p&gt;

&lt;p&gt;Other checks cover missing linked files, uncovered expected file patterns, missing acceptance evidence, and unknown requirement IDs. The implementation uses file-pattern matching for expected files, so a pattern such as &lt;code&gt;src/checkout_audit/*.py&lt;/code&gt; must match at least one linked path.&lt;/p&gt;

&lt;p&gt;This is the central lesson: a traceability tool can make an omission visible, but it cannot manufacture evidence. A change map that says a test passed is still an assertion that your workflow must produce and reviewers must trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the split between spec and change map matters
&lt;/h2&gt;

&lt;p&gt;The requirement spec is the review request. It describes what must be true and what evidence should exist. The change map is the implementation record. It says which files changed, which tests were reported as passed, and why acceptance criteria are considered covered.&lt;/p&gt;

&lt;p&gt;Keeping those documents separate gives you a stable question for code review: does this change map prove the requirements, or does it only describe the files that were edited?&lt;/p&gt;

&lt;p&gt;SpecTrace builds a &lt;code&gt;RequirementTrace&lt;/code&gt; for each requirement. It gathers matching files, tests, and acceptance entries, then applies findings for each missing or invalid piece. A requirement is marked passed only when that list of findings is empty. Global cross-reference errors are also included in the final result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes to plan for
&lt;/h2&gt;

&lt;h3&gt;
  
  
  A valid JSON file can still describe weak evidence
&lt;/h3&gt;

&lt;p&gt;Validation checks the data model and references. It does not inspect your application source, rerun a test named in the change map, or verify that a summary is truthful. Treat the map as review material, not as an attestation generated by the verifier.&lt;/p&gt;

&lt;h3&gt;
  
  
  A passing trace does not judge code quality
&lt;/h3&gt;

&lt;p&gt;The project explicitly does not use an LLM to evaluate semantic correctness. It also cannot determine whether a real pull request is correct beyond the supplied evidence map. Pair it with normal tests, code review, and domain-specific checks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inputs may contain sensitive data
&lt;/h3&gt;

&lt;p&gt;The repository's &lt;a href="https://github.com/paladini/spectrace-ai-coding/blob/main/SECURITY.md" rel="noopener noreferrer"&gt;security policy&lt;/a&gt; warns that requirement documents, file paths, test logs, and AI coding evidence can be sensitive. Use synthetic examples in issues and tests. Do not place tokens, private source code, customer paths, or production logs into a public change map.&lt;/p&gt;

&lt;h3&gt;
  
  
  A failed verification is intentionally not a test failure
&lt;/h3&gt;

&lt;p&gt;The command returns status 2 when requirements fail or global findings exist. That is distinct from an input validation error, which returns status 1. Your CI wrapper should preserve that distinction if it reports remediation hints.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducible verification
&lt;/h2&gt;

&lt;p&gt;Run the repository's tests after the example commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;unittest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;discover&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tests&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The current repository test suite contains seven tests. The bundled command path also verifies two requirements, writes Markdown, JSON, and SARIF files, and passes with status 0 in the checked working tree.&lt;/p&gt;

&lt;p&gt;For a practical CI integration, keep the spec and change map in the repository, run &lt;code&gt;validate&lt;/code&gt; first, then run &lt;code&gt;verify&lt;/code&gt; and upload &lt;code&gt;build\spectrace.sarif.json&lt;/code&gt; using your platform's SARIF support. The exact upload action is platform-specific and is outside SpecTrace's scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does SpecTrace need an AI provider?
&lt;/h3&gt;

&lt;p&gt;No. Version 0.1.0 uses Python's standard library and does not call model APIs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does it replace tests?
&lt;/h3&gt;

&lt;p&gt;No. It checks that required tests are represented in the change map with a &lt;code&gt;passed&lt;/code&gt; status. It does not run those tests itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can it prove that an AI-generated change is correct?
&lt;/h3&gt;

&lt;p&gt;No. It proves only that the supplied evidence map covers the declared requirements according to the implemented rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why generate SARIF?
&lt;/h3&gt;

&lt;p&gt;SARIF gives CI and code-scanning tools a common format for displaying findings. The Markdown and JSON files remain useful for local review and automation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;The smallest useful safeguard for AI-assisted development is often not another model call. It is a concrete link between a requirement, the files that address it, the tests that support it, and the acceptance evidence a reviewer can inspect.&lt;/p&gt;

&lt;p&gt;SpecTrace makes that link deterministic and reviewable. Start with one high-value requirement, keep the evidence map honest, and expand the check only when your team can explain where each piece of evidence comes from.&lt;/p&gt;

&lt;p&gt;Have you found a reliable way to generate and review change maps in your AI coding workflow, or do you still rely mainly on the diff and test output?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;AI assistance disclosure: AI was used to help organize and edit this tutorial. The commands, repository details, implementation claims, and verification results were checked against the public SpecTrace repository and its bundled examples.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>tutorial</category>
      <category>traceability</category>
    </item>
    <item>
      <title>Build a Typed Training Data Client in TypeScript with intervals-icu</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Wed, 29 Jul 2026 15:36:16 +0000</pubDate>
      <link>https://dev.to/paladini/build-a-typed-training-data-client-in-typescript-with-intervals-icu-8fc</link>
      <guid>https://dev.to/paladini/build-a-typed-training-data-client-in-typescript-with-intervals-icu-8fc</guid>
      <description>&lt;p&gt;If your training dashboard starts as one HTTP request and grows into athletes, activities, wellness, workouts, gear, and performance data, a hand-written fetch wrapper becomes expensive to maintain. Every new endpoint adds another URL, another response shape, and another place to get authentication or retry behavior wrong.&lt;/p&gt;

&lt;p&gt;This tutorial shows a small, reproducible path with &lt;a href="https://github.com/paladini/node-intervals-icu" rel="noopener noreferrer"&gt;intervals-icu&lt;/a&gt;, an open-source TypeScript client for the &lt;a href="https://intervals.icu/api/v1/docs" rel="noopener noreferrer"&gt;Intervals.icu API&lt;/a&gt;. The goal is not to build a complete training application. It is to establish a typed client, choose the right authentication boundary, call one service, and understand what changes when you move from version 1 to version 2 of the library.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Install the stable npm package, create an IntervalsClient with an API key or OAuth access token, and use service accessors such as client.athletes or client.activities. Version 2 uses typed service methods, retries selected transient failures, and defaults requests to the authenticated athlete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 18 or newer.&lt;/li&gt;
&lt;li&gt;npm.&lt;/li&gt;
&lt;li&gt;An Intervals.icu account with an API key, or an OAuth access token for an application acting for other users.&lt;/li&gt;
&lt;li&gt;A TypeScript project that can run ESM modules.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The published package is intervals-icu version 2.2.1, and its package metadata declares Node.js &amp;gt;=18.0.0. The repository is public and licensed under MIT. The examples below target that stable package version, not an unreleased default-branch change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install the stable client
&lt;/h2&gt;

&lt;p&gt;Create a small project and pin the package version used in this tutorial:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;intervals-demo
&lt;span class="nb"&gt;cd &lt;/span&gt;intervals-demo
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;intervals-icu@2.2.1
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; typescript tsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The package publishes both ESM and CommonJS entry points and exposes TypeScript declarations from its package root. Add a script so a .ts file can run without a separate build step:&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;"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;"module"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&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;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tsx src/index.ts"&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;h2&gt;
  
  
  Create the smallest useful client
&lt;/h2&gt;

&lt;p&gt;Create src/index.ts. Keep the credential outside source control. The placeholder below is intentionally not a real key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;IntervalsClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;intervals-icu&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;INTERVALS_API_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Set INTERVALS_API_KEY before running this example&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;IntervalsClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;athleteId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxRetries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;retryDelayMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;athlete&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;athletes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAthlete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;athlete&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;athlete&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ftp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;athlete&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ftp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it with an environment variable rather than placing the key in the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;INTERVALS_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-key npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Windows PowerShell, use the equivalent session-scoped variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;INTERVALS_API_KEY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-key"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;npm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The expected result is an object containing the authenticated athlete's fields returned by the API. The exact values depend on the account, so a reproducible verification is to confirm that id is present and that the command exits successfully without printing the credential.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why service accessors matter
&lt;/h2&gt;

&lt;p&gt;The client groups operations by resource instead of putting every method on one large facade. The same client can read activities, events, wellness records, and workouts through separate accessors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;recentActivities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listActivities&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;oldest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-01-01&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;newest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-01-31&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listEvents&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;oldest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-01-01&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;newest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-01-31&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wellness&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wellness&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listWellness&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;oldest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-01-01&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;newest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-01-31&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;activities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;recentActivities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;wellnessRecords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;wellness&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This layout is more than naming preference. It gives each resource group a discoverable boundary and lets the compiler catch common mistakes such as calling a method that belonged to the old facade API. The repository exports more than 100 typed methods across 16 service groups, including routes, gear, weather, custom items, fitness, performance, and search.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add a typed activity query
&lt;/h2&gt;

&lt;p&gt;Activity IDs are strings in version 2. Use the service accessor and an ID such as i55610271:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;activity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getActivity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;i55610271&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;startDateLocal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;start_date_local&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not copy the old numeric-ID form from a version 1 example. The project's migration guide documents the breaking change and the replacement method names. That guide is worth reading before upgrading because all top-level facade methods were removed in version 2.&lt;/p&gt;

&lt;h2&gt;
  
  
  API keys, OAuth, and retry behavior
&lt;/h2&gt;

&lt;p&gt;Use an API key for a personal integration. Use accessToken when your application obtains an OAuth bearer token for users:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;IntervalsClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;INTERVALS_ACCESS_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;athleteId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;i12345&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two credential options represent different trust boundaries. An API key is a personal secret and should stay on a server or local process you control. An OAuth access token is still a secret, even though it represents delegated access. Do not ship either value in browser JavaScript, commit it to Git, or include it in logs.&lt;/p&gt;

&lt;p&gt;Version 2.2.0 added support for the server's Retry-After response and jittered exponential backoff. The client retries 429 and 5xx responses by default, and maxRetries and retryDelayMs let you tune that policy. Retries reduce sensitivity to temporary failures, but they do not make a request safe to repeat in every business workflow. Be especially careful with mutating methods and design your own application-level idempotency where the API operation requires it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common failure modes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The client constructs but the request fails
&lt;/h3&gt;

&lt;p&gt;Constructing IntervalsClient does not prove that the credential is valid. Verify the environment variable, token scope, athlete ID, and the account's access to the requested resource. A request is the meaningful smoke test.&lt;/p&gt;

&lt;h3&gt;
  
  
  A version 1 example no longer compiles
&lt;/h3&gt;

&lt;p&gt;Replace client.getAthlete() with client.athletes.getAthlete(), client.getEvents() with client.events.listEvents(), and numeric activity IDs with strings. These are intentional version 2 changes, not TypeScript configuration problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  A request keeps retrying
&lt;/h3&gt;

&lt;p&gt;Inspect the final IntervalsAPIError, including its HTTP status and retry information. A rate limit response may tell you when to try again, while a persistent 4xx response generally needs a credential, input, or permission change. Setting maxRetries to 0 can make failure timing clearer during local debugging.&lt;/p&gt;

&lt;h3&gt;
  
  
  The data looks different from your expectation
&lt;/h3&gt;

&lt;p&gt;Treat the API response as an external contract. Keep the package types, the &lt;a href="https://intervals.icu/api/v1/docs" rel="noopener noreferrer"&gt;Intervals.icu API documentation&lt;/a&gt;, and your own validation close to the integration. Types improve the calling code, but they cannot guarantee that a remote service will never change.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does this client require an API key?
&lt;/h3&gt;

&lt;p&gt;Every authenticated API request needs either an API key or an OAuth access token. The configuration makes apiKey optional because OAuth is supported, not because the API is anonymous.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use it for activities and wellness data?
&lt;/h3&gt;

&lt;p&gt;Yes. Version 2 exposes separate accessors for activities, wellness, events, workouts, and other resource groups. Check the current API permissions before requesting or mutating data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does it run in the browser?
&lt;/h3&gt;

&lt;p&gt;The package is a Node.js client with credentials and server-oriented HTTP behavior. Do not put a personal API key in a browser bundle. If you need a browser UI, put a controlled backend between the UI and Intervals.icu and expose only the operations your application needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;intervals-icu gives a TypeScript application a clear starting point for Intervals.icu integration: install a stable version, keep credentials server-side, use resource-specific services, and let the typed client handle the repetitive HTTP layer. Start with one read-only request, verify the returned shape, and add writes only after you understand the account permissions and retry consequences.&lt;/p&gt;

&lt;p&gt;Have you found a better boundary for exposing training data to a frontend: a narrow backend API per feature, or a more general proxy with stricter authorization rules?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;AI assistance disclosure:&lt;/strong&gt; AI assistance was used to organize and edit this tutorial. The project documentation, package metadata, migration guide, changelog, and example API shape were checked against current primary sources before publication.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>typescript</category>
      <category>api</category>
      <category>tutorial</category>
      <category>fitness</category>
    </item>
    <item>
      <title>Give Your AI Assistant a Local Profile with mcp-me</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Wed, 29 Jul 2026 13:12:06 +0000</pubDate>
      <link>https://dev.to/paladini/give-your-ai-assistant-a-local-profile-with-mcp-me-34d0</link>
      <guid>https://dev.to/paladini/give-your-ai-assistant-a-local-profile-with-mcp-me-34d0</guid>
      <description>&lt;p&gt;Every new AI coding session starts with the same small interview: What languages do you use? Which projects do you maintain? What kind of writing do you prefer? Repeating that context is tedious, and putting a complete personal profile into every prompt is difficult to maintain.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/paladini/mcp-me" rel="noopener noreferrer"&gt;mcp-me&lt;/a&gt; takes a different approach. It stores a structured profile as local YAML files and exposes that profile through the &lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt;. An MCP-compatible assistant can then read resources such as &lt;code&gt;me://skills&lt;/code&gt;, &lt;code&gt;me://projects&lt;/code&gt;, and &lt;code&gt;me://career&lt;/code&gt; when the request benefits from personal context.&lt;/p&gt;

&lt;p&gt;This tutorial shows a small, reproducible setup. You will install the released &lt;code&gt;mcp-me&lt;/code&gt; package, initialize a profile, validate it, configure an MCP server, and add an instruction file that tells an agent when to consult the profile.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Install &lt;code&gt;mcp-me&lt;/code&gt; with Node.js 20 or later, run &lt;code&gt;mcp-me init&lt;/code&gt;, validate the generated YAML, and register this MCP server with your assistant:&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;"me"&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;"mcp-me"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"serve"&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;Then add an &lt;code&gt;AGENTS.md&lt;/code&gt; file that tells your agent to call &lt;code&gt;ask_about_me&lt;/code&gt; before answering requests where your background matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 20 or later&lt;/li&gt;
&lt;li&gt;An MCP-compatible assistant, such as Claude Desktop, Cursor, Windsurf, or VS Code with GitHub Copilot&lt;/li&gt;
&lt;li&gt;A writable local directory for your profile&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The package declares Node.js &lt;code&gt;&amp;gt;=20.0.0&lt;/code&gt; and uses the MIT license in its current package metadata. The commands below use the current npm release, &lt;code&gt;mcp-me@0.6.0&lt;/code&gt;, which matches the repository's current &lt;code&gt;main&lt;/code&gt; package metadata.&lt;/p&gt;

&lt;p&gt;Check your Node.js version first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Install and initialize the profile
&lt;/h2&gt;

&lt;p&gt;For a globally available CLI, install the package with npm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; mcp-me@0.6.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The project also supports &lt;code&gt;npx&lt;/code&gt; when you want to try it without a global install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx mcp-me@0.6.0 &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the default profile directory and its templates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mcp-me init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, the profile lives in &lt;code&gt;~/.mcp-me&lt;/code&gt;. The command creates &lt;code&gt;.mcp-me.yaml&lt;/code&gt; plus YAML files for identity, career, skills, interests, personality, goals, projects, and frequently asked questions. You can initialize another directory by passing it as the command argument:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mcp-me init ./my-profile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the generated &lt;code&gt;.mcp-me.yaml&lt;/code&gt; and uncomment only the sources you want to use. A minimal configuration can look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;generators&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;github&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-username&lt;/span&gt;
  &lt;span class="na"&gt;devto&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-username&lt;/span&gt;

&lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;github&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-username&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generator section describes data to collect when you run &lt;code&gt;generate&lt;/code&gt;. The plugin section describes live integrations used while the MCP server is running. Keeping these concepts separate helps you decide whether a value should be a local snapshot or a live lookup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generate and validate local data
&lt;/h2&gt;

&lt;p&gt;With the configuration in place, generate your profile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mcp-me generate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The README documents public sources such as GitHub and DEV.to, along with many optional generators. Most generators do not need API keys, but the source-specific requirements still apply. For integrations that require credentials, read the generated comments and the project documentation before enabling them.&lt;/p&gt;

&lt;p&gt;Validate the profile before connecting an assistant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mcp-me validate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command checks the profile YAML files against the project's schemas. A successful run ends with &lt;code&gt;All profile files are valid!&lt;/code&gt;. Validation catches malformed YAML and schema problems before an assistant receives incomplete or unexpected context.&lt;/p&gt;

&lt;p&gt;If you prefer not to install the CLI globally, use the same commands through &lt;code&gt;npx&lt;/code&gt; and pass the profile directory explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; mcp-me@0.6.0 validate ./my-profile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Connect the MCP server
&lt;/h2&gt;

&lt;p&gt;The server defaults to &lt;code&gt;~/.mcp-me&lt;/code&gt;, so the simplest MCP configuration does not need a profile path. Add the following entry to the configuration used by your client:&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;"me"&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;"mcp-me"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"serve"&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;For a manually installed CLI, you can use &lt;code&gt;mcp-me&lt;/code&gt; as the command instead of &lt;code&gt;npx&lt;/code&gt;. If the profile is stored elsewhere, set &lt;code&gt;MCP_ME_PROFILE_DIR&lt;/code&gt; in the client environment or pass the directory to &lt;code&gt;mcp-me serve&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The project documents client-specific differences. For example, VS Code uses a &lt;code&gt;servers&lt;/code&gt; key in its MCP configuration, while the configuration above uses &lt;code&gt;mcpServers&lt;/code&gt;. Check the &lt;a href="https://github.com/paladini/mcp-me/blob/main/docs/ai-instructions.md" rel="noopener noreferrer"&gt;configuration guidance in the repository&lt;/a&gt; before copying the entry into a particular client.&lt;/p&gt;

&lt;h2&gt;
  
  
  Teach the agent when to use your profile
&lt;/h2&gt;

&lt;p&gt;Connecting the server makes the resources available, but an agent still needs a reason to consult them. Add an &lt;code&gt;AGENTS.md&lt;/code&gt; file to the project where you want this behavior:&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;# Agent Instructions&lt;/span&gt;

You have access to an MCP server called &lt;span class="sb"&gt;`me`&lt;/span&gt; that exposes my personal profile.

Always use &lt;span class="sb"&gt;`ask_about_me`&lt;/span&gt; or read the MCP resources below before answering any
request that could benefit from knowing who I am, including writing, code review,
documentation, career questions, and open-source contributions.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repository includes a longer template at &lt;code&gt;templates/AGENTS.md&lt;/code&gt;. You can copy it after a global npm installation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;npm root &lt;span class="nt"&gt;-g&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/mcp-me/templates/AGENTS.md"&lt;/span&gt; ./AGENTS.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This file is guidance for the agent, not a security boundary. A model may still choose the wrong tool or misunderstand a request. Keep the instructions narrow, review them like any other project configuration, and avoid putting secrets into profile files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the result
&lt;/h2&gt;

&lt;p&gt;Restart or reload your MCP client after changing its configuration. Then ask a question that requires profile context, such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Draft a short README section that reflects my TypeScript projects and open-source work.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The expected result is not a specific sentence. The useful verification is that the assistant can consult the &lt;code&gt;me&lt;/code&gt; server and produce an answer grounded in the profile instead of asking you to restate the same background.&lt;/p&gt;

&lt;p&gt;You can also verify the local server directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mcp-me serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server is intended to run as an MCP process, so a terminal may appear idle while it waits for protocol messages. Stop it with &lt;code&gt;Ctrl+C&lt;/code&gt; after confirming that the process starts without a profile or configuration error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this works
&lt;/h2&gt;

&lt;p&gt;The profile is organized into stable resources rather than one large prompt. Identity, skills, career, projects, and other categories can be read independently. That gives an assistant a smaller, more targeted context surface for each request.&lt;/p&gt;

&lt;p&gt;The same separation also makes updates local and reviewable. You can edit &lt;code&gt;skills.yaml&lt;/code&gt; without rewriting an instruction file, regenerate public-source data when needed, and keep project-specific rules next to the project that owns them. The MCP server then provides a consistent interface for compatible clients.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes and limitations
&lt;/h2&gt;

&lt;p&gt;If &lt;code&gt;mcp-me&lt;/code&gt; is not found, confirm that Node.js 20 or later is installed and that the global npm binary directory is on your PATH. If you use &lt;code&gt;npx&lt;/code&gt;, include &lt;code&gt;-y&lt;/code&gt; in non-interactive setup scripts.&lt;/p&gt;

&lt;p&gt;If validation fails, read the file and line reported by &lt;code&gt;mcp-me validate&lt;/code&gt;. Do not assume that valid YAML is valid profile data. The schemas define the accepted structure.&lt;/p&gt;

&lt;p&gt;If generation fails for one source, disable that source and test the remaining configuration. Some services require tokens or have rate limits. The README says that most generators use public APIs without keys, not that every source is unauthenticated.&lt;/p&gt;

&lt;p&gt;The local-first design reduces unnecessary cloud transfer, but it does not make the profile harmless. Your local files can contain personal and career information, and an MCP client can expose that information to the model you connect. Use filesystem permissions, keep secrets out of YAML, and understand the data handling policy of your assistant before enabling the server.&lt;/p&gt;

&lt;p&gt;The project is also not a memory system that guarantees perfect recall. It provides structured context and tools. The assistant still decides when to use them, and the profile is only as accurate as its files and generators.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does mcp-me store my profile in the cloud?
&lt;/h3&gt;

&lt;p&gt;The project documents the profile as local YAML data and says the MCP server reads from disk. Your assistant still receives whatever profile content it requests, so local storage does not eliminate model-provider data handling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use a custom profile directory?
&lt;/h3&gt;

&lt;p&gt;Yes. Pass a directory to &lt;code&gt;init&lt;/code&gt;, &lt;code&gt;validate&lt;/code&gt;, or &lt;code&gt;serve&lt;/code&gt;, or set &lt;code&gt;MCP_ME_PROFILE_DIR&lt;/code&gt; for the default profile path used by the server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need an API key for GitHub or DEV.to?
&lt;/h3&gt;

&lt;p&gt;The documented generators use public APIs for many sources and do not require keys in the common case. Optional live plugins can have different authentication requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use mcp-me with more than one assistant?
&lt;/h3&gt;

&lt;p&gt;Yes. Configure the same local MCP server in each compatible client. Review each client's configuration format and data-sharing behavior separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;mcp-me turns repeated personal context into a local, structured MCP profile. Start with a few accurate YAML files, validate them, connect the server, and add agent instructions only after you understand what data should be available.&lt;/p&gt;

&lt;p&gt;This tutorial was prepared with AI assistance. The repository documentation, package metadata, live package metadata, and the commands shown here were checked during preparation; the prose was reviewed for accuracy and limitations.&lt;/p&gt;

&lt;p&gt;What personal context would save you the most repetition in an AI coding workflow: skills, project history, career details, or something else?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>tutorial</category>
      <category>yaml</category>
    </item>
    <item>
      <title>Translate Git Commit Messages Offline Without Rewriting Code</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Tue, 28 Jul 2026 19:45:07 +0000</pubDate>
      <link>https://dev.to/paladini/translate-git-commit-messages-offline-without-rewriting-code-49he</link>
      <guid>https://dev.to/paladini/translate-git-commit-messages-offline-without-rewriting-code-49he</guid>
      <description>&lt;p&gt;A repository can have clean code and still have a difficult history. One commit says &lt;code&gt;fix: corrige timeout&lt;/code&gt;, another says &lt;code&gt;feat: add retry logic&lt;/code&gt;, and the next was generated by an AI coding assistant in a third language.&lt;/p&gt;

&lt;p&gt;That inconsistency makes &lt;code&gt;git log&lt;/code&gt;, release-note preparation, blame investigation, and onboarding harder than they need to be. Manually editing hundreds of messages is not realistic, while sending an entire private history to an external translation API may violate your team's privacy expectations.&lt;/p&gt;

&lt;p&gt;This tutorial shows how to use &lt;a href="https://github.com/paladini/git-translate-commits" rel="noopener noreferrer"&gt;git-translate-commits&lt;/a&gt;, an open-source Python CLI, to preview and normalize Git commit messages with a local translation engine. We will test the operation in a disposable clone first because changing a commit message necessarily changes its commit hash.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Install the CLI, create a disposable clone, and start with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git-translate-commits &lt;span class="nt"&gt;--lang&lt;/span&gt; en &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default engine uses Argos Translate. It downloads the required language model on first use and can then translate locally without an API key. Do not remove &lt;code&gt;--dry-run&lt;/code&gt; until you have inspected the proposed messages and coordinated any history rewrite with everyone using the repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why multilingual Git history becomes a maintenance problem
&lt;/h2&gt;

&lt;p&gt;Mixed-language commits are not inherently wrong. The problem appears when a team expects one searchable language but people, automation, and coding agents produce several.&lt;/p&gt;

&lt;p&gt;Consider a production incident. You search for "payment timeout", but the relevant fix was committed as &lt;code&gt;corrige tempo limite do pagamento&lt;/code&gt;. Git cannot retrieve the concept if the words do not match. The same inconsistency affects changelog generation and tools that classify Conventional Commits.&lt;/p&gt;

&lt;p&gt;The safest time to establish a commit-language policy is before merging. Existing repositories, however, may already contain years of mixed messages. A controlled history rewrite can normalize that history, provided the team understands the consequences.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;The current package metadata requires Python 3.10 or newer. You also need Git and either &lt;code&gt;pipx&lt;/code&gt;, &lt;code&gt;uv&lt;/code&gt;, or &lt;code&gt;pip&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Install the command in an isolated environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pipx &lt;span class="nb"&gt;install &lt;/span&gt;git-translate-commits

&lt;span class="c"&gt;# Alternative with uv&lt;/span&gt;
uv tool &lt;span class="nb"&gt;install &lt;/span&gt;git-translate-commits
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The published PyPI package is currently version &lt;code&gt;1.0.1&lt;/code&gt;. During verification for this tutorial, the installed command responded to &lt;code&gt;--version&lt;/code&gt;, although it printed &lt;code&gt;v1.0.0&lt;/code&gt;. Treat PyPI metadata as the package-release version until that CLI display mismatch is corrected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Work in a disposable clone
&lt;/h2&gt;

&lt;p&gt;Do not test a history-rewriting tool in your only local copy. Clone the repository into a separate directory and keep the original remote unchanged:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/your-org/your-repository.git history-translation-test
&lt;span class="nb"&gt;cd &lt;/span&gt;history-translation-test
git status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the placeholder URL with a repository you are authorized to modify. Confirm that the working tree is clean before continuing.&lt;/p&gt;

&lt;p&gt;For a shared repository, announce the experiment before anybody bases new work on rewritten commits. Even a correct rewrite creates new commit identifiers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Preview the translation
&lt;/h2&gt;

&lt;p&gt;The required option is &lt;code&gt;--lang&lt;/code&gt;. Use a language code such as &lt;code&gt;en&lt;/code&gt;, &lt;code&gt;es&lt;/code&gt;, or &lt;code&gt;pt-BR&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Run the default local engine in dry-run mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git-translate-commits &lt;span class="nt"&gt;--lang&lt;/span&gt; en &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the critical safety step. &lt;code&gt;--dry-run&lt;/code&gt; shows what would change without modifying the repository.&lt;/p&gt;

&lt;p&gt;Review the preview for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;technical terms that should remain unchanged;&lt;/li&gt;
&lt;li&gt;issue identifiers such as &lt;code&gt;#123&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;Conventional Commit prefixes such as &lt;code&gt;feat:&lt;/code&gt; and &lt;code&gt;fix:&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;names, product terminology, and acronyms;&lt;/li&gt;
&lt;li&gt;messages already written in the target language.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By default, the CLI is documented to skip messages already detected in the target language and preserve Conventional Commit prefixes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Reduce the scope when necessary
&lt;/h2&gt;

&lt;p&gt;You rarely need to translate everything on the first attempt. Filters make a smaller experiment easier to review.&lt;/p&gt;

&lt;p&gt;For example, preview commits from a date forward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git-translate-commits &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--lang&lt;/span&gt; en &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--since&lt;/span&gt; &lt;span class="s2"&gt;"2026-01-01"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also select an author:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git-translate-commits &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--lang&lt;/span&gt; en &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--author&lt;/span&gt; &lt;span class="s2"&gt;"developer@example.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Other documented filters include &lt;code&gt;--until&lt;/code&gt;, &lt;code&gt;--branch&lt;/code&gt;, and &lt;code&gt;--all-branches&lt;/code&gt;. Start with the current branch unless you have a reviewed migration plan for the entire repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Understand what the tool preserves
&lt;/h2&gt;

&lt;p&gt;git-translate-commits changes commit messages rather than source files. Its documented preservation rules include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;file contents;&lt;/li&gt;
&lt;li&gt;author and committer names;&lt;/li&gt;
&lt;li&gt;email addresses and timestamps;&lt;/li&gt;
&lt;li&gt;Conventional Commit prefixes;&lt;/li&gt;
&lt;li&gt;issue references;&lt;/li&gt;
&lt;li&gt;Git trailers such as &lt;code&gt;Co-authored-by&lt;/code&gt; and &lt;code&gt;Signed-off-by&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Preserving those fields does not preserve commit hashes. A commit hash incorporates the commit message, so changing the message produces a different hash. Every descendant commit is rewritten as well.&lt;/p&gt;

&lt;p&gt;The CLI creates a backup branch by default and writes &lt;code&gt;.git-translate-log.json&lt;/code&gt; with the old-to-new mapping. Keep both until the rewritten history has been reviewed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Apply the rewrite only after review
&lt;/h2&gt;

&lt;p&gt;Once the dry run is correct and the team has agreed on the migration window, run the same command without &lt;code&gt;--dry-run&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git-translate-commits &lt;span class="nt"&gt;--lang&lt;/span&gt; en
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command asks for confirmation unless &lt;code&gt;--force&lt;/code&gt; is used. Avoid &lt;code&gt;--force&lt;/code&gt; during an initial migration because the prompt is a useful final pause.&lt;/p&gt;

&lt;p&gt;After the command completes, inspect the result locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="nt"&gt;--decorate&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 20
git branch &lt;span class="nt"&gt;--list&lt;/span&gt;
git status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare representative messages with the preview, confirm that the backup branch exists, and inspect &lt;code&gt;.git-translate-log.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Do not force-push immediately. Run your tests and compare the rewritten tree with the original branch first. The file trees should remain equivalent even though commit hashes differ.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optional: use an LLM translation engine
&lt;/h2&gt;

&lt;p&gt;The default local engine is the privacy-oriented path. The project also offers an optional &lt;code&gt;llm&lt;/code&gt; extra through LiteLLM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pipx &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"git-translate-commits[llm]"&lt;/span&gt;
git-translate-commits &lt;span class="nt"&gt;--lang&lt;/span&gt; en &lt;span class="nt"&gt;--engine&lt;/span&gt; llm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This mode can use OpenAI, Anthropic, or an OpenAI-compatible provider. It changes the privacy, cost, and credential boundaries of the workflow. Prefer environment variables over command-line API keys, and confirm whether commit messages are allowed to leave your environment.&lt;/p&gt;

&lt;p&gt;An OpenAI-compatible local server can provide a different local path, but model behavior and compatibility depend on that server. Validate it separately rather than assuming it behaves like the default Argos engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to verify that only history metadata changed
&lt;/h2&gt;

&lt;p&gt;Before rewriting, record the tree hash of the branch tip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git rev-parse HEAD^&lt;span class="o"&gt;{&lt;/span&gt;tree&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the same command after translation. If the tree hash is identical, the checked-in file snapshot at the branch tip is unchanged. This does not replace tests, but it is a useful deterministic check that the final source tree stayed the same.&lt;/p&gt;

&lt;p&gt;Also verify:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the expected number of commits still exists;&lt;/li&gt;
&lt;li&gt;author identities and timestamps are preserved;&lt;/li&gt;
&lt;li&gt;issue references and trailers remain intact;&lt;/li&gt;
&lt;li&gt;application tests pass;&lt;/li&gt;
&lt;li&gt;the backup branch resolves to the original history;&lt;/li&gt;
&lt;li&gt;collaborators know that their existing branches need reconciliation.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Limitations and safer alternatives
&lt;/h2&gt;

&lt;p&gt;History rewriting is disruptive. Open pull requests, signed commits, release tags, CI references, and external links to commits can be affected. A backup branch helps recovery, but it does not remove the coordination cost.&lt;/p&gt;

&lt;p&gt;For an active public repository, keeping existing history and enforcing one language only for future commits may be the better choice. A commit-message hook, contribution guideline, or pull-request squash policy can prevent new drift without invalidating old hashes.&lt;/p&gt;

&lt;p&gt;Local neural translation also has quality limits. Domain-specific messages may need manual review. The optional LLM engine may improve context in some cases, but it introduces a provider and a data-transfer boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does this translate source code?
&lt;/h3&gt;

&lt;p&gt;No. The tool targets Git commit messages. You should still compare tree hashes and run tests before sharing rewritten history.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I preview without changing anything?
&lt;/h3&gt;

&lt;p&gt;Yes. Use &lt;code&gt;--dry-run&lt;/code&gt;, and keep it enabled while tuning language, branch, author, and date filters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Will commit hashes change?
&lt;/h3&gt;

&lt;p&gt;Yes. The message is part of the commit object, so changing it creates a new hash.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the default engine need an API key?
&lt;/h3&gt;

&lt;p&gt;No. The default Argos Translate engine downloads language data on first use and operates locally afterward.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I rewrite a shared default branch?
&lt;/h3&gt;

&lt;p&gt;Only with explicit team coordination and a recovery plan. For many active repositories, a future-only commit-language policy is safer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;A consistent Git history is useful, but consistency is not worth a surprise rewrite. Start in a disposable clone, use the offline dry run, narrow the scope, verify tree hashes, and involve the team before changing a shared branch.&lt;/p&gt;

&lt;p&gt;git-translate-commits turns a repetitive editing problem into a reviewable workflow. The most important feature is not automatic translation. It is the ability to inspect the plan before accepting a destructive change.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Disclosure: This article was researched, fact-checked, and drafted with AI assistance using the current primary project sources linked above.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Would you rewrite an existing multilingual history, or enforce one commit language only from today forward?&lt;/p&gt;

</description>
      <category>git</category>
      <category>python</category>
      <category>tutorial</category>
      <category>i18n</category>
    </item>
    <item>
      <title>Sync AI Agent Rules Across Repositories with GitHub Actions</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Tue, 28 Jul 2026 19:11:04 +0000</pubDate>
      <link>https://dev.to/paladini/sync-ai-agent-rules-across-repositories-with-github-actions-5b9h</link>
      <guid>https://dev.to/paladini/sync-ai-agent-rules-across-repositories-with-github-actions-5b9h</guid>
      <description>&lt;p&gt;Your team improves an &lt;code&gt;AGENTS.md&lt;/code&gt; instruction in one repository. A week later, another service still has the old rule. Cursor configuration has drifted too, and the shared Claude prompt was copied into only three of six projects.&lt;/p&gt;

&lt;p&gt;This is an ordinary configuration-management problem hiding inside an AI tooling workflow. Copying files by hand does not scale, but pushing changes directly to protected default branches is not a good answer either.&lt;/p&gt;

&lt;p&gt;In this tutorial, we will use &lt;a href="https://github.com/paladini/team-ai-sync" rel="noopener noreferrer"&gt;team-ai-sync&lt;/a&gt; to keep AI agent instructions, prompts, and editor rules consistent across repositories. The workflow starts in dry-run mode, then distributes changes through normal pull requests so every update remains visible and reviewable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we are building
&lt;/h2&gt;

&lt;p&gt;We will use one repository as the source of truth for shared AI guidance. A GitHub Actions workflow in that repository will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read a versioned synchronization manifest.&lt;/li&gt;
&lt;li&gt;Compare the selected files with their destinations.&lt;/li&gt;
&lt;li&gt;Preview changes without writing anything.&lt;/li&gt;
&lt;li&gt;Create or update a pull request in each target repository after dry-run verification.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This approach is useful for files such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;AGENTS.md&lt;/code&gt; and &lt;code&gt;CLAUDE.md&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;GitHub Copilot instruction files&lt;/li&gt;
&lt;li&gt;Cursor and Windsurf rules&lt;/li&gt;
&lt;li&gt;reusable prompts&lt;/li&gt;
&lt;li&gt;team-specific documentation for coding agents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tool synchronizes files, not hidden model context. It does not decide whether AI authored anything, and it should never be used to distribute secrets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a GitHub repository that will hold the canonical files;&lt;/li&gt;
&lt;li&gt;one or more target repositories;&lt;/li&gt;
&lt;li&gt;permission to create branches and pull requests in those targets;&lt;/li&gt;
&lt;li&gt;a fine-grained personal access token or GitHub App token for cross-repository access.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The normal workflow &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; can read the source repository. Cross-repository writes require a separate token because the built-in token is scoped to the repository running the workflow.&lt;/p&gt;

&lt;p&gt;For a fine-grained token, grant access only to the target repositories. The documented minimum is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Contents: read and write&lt;/li&gt;
&lt;li&gt;Pull requests: read and write&lt;/li&gt;
&lt;li&gt;Metadata: read&lt;/li&gt;
&lt;li&gt;Issues: read and write only when you want the workflow to apply labels&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Store the token as a repository or organization Actions secret. In this example, the secret is named &lt;code&gt;TEAM_SYNC_ADMIN_PAT&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Create the canonical files
&lt;/h2&gt;

&lt;p&gt;Suppose your source repository has this structure:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;shared-ai-guidance/
├── AGENTS.md
├── prompts/
│   └── review.md
└── .github/
    └── sync-config.json
~~~

Keep these files in Git like any other engineering policy. Changes then have authors, reviews, commit history, and an obvious rollback path.

## Step 2: Define the synchronization manifest

Create `.github/sync-config.json`:

~~~json
{
  "version": 1,
  "defaults": {
    "branchName": "chore/sync-ai-guidance",
    "commitMessage": "chore: sync shared AI guidance",
    "prTitle": "chore: sync shared AI guidance",
    "deleteOrphans": false
  },
  "targets": [
    {
      "repo": "your-org/api-service",
      "files": [
        {
          "source": "AGENTS.md",
          "destination": "AGENTS.md"
        },
        {
          "source": "prompts/review.md",
          "destination": ".github/prompts/review.md"
        }
      ]
    },
    {
      "repo": "your-org/web-app",
      "files": [
        {
          "source": "AGENTS.md",
          "destination": "AGENTS.md"
        }
      ]
    }
  ]
}
~~~

Replace the placeholder repository names with repositories you control.

The explicit source and destination paths make the contract easy to audit. team-ai-sync also validates paths: absolute paths, traversal through `..`, and `.git` paths are rejected. `deleteOrphans` is disabled by default, which makes initial adoption safer because an incomplete manifest will not remove files from target repositories.

## Step 3: Add the GitHub Actions workflow

Create `.github/workflows/sync-ai-guidance.yml` in the source repository:

~~~yaml
name: Sync AI guidance

on:
  workflow_dispatch:
  push:
    branches:
      - main
    paths:
      - AGENTS.md
      - prompts/**
      - .github/sync-config.json

permissions:
  contents: read

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - name: Check out the source repository
        uses: actions/checkout@v4

      - name: Preview synchronization
        uses: paladini/team-ai-sync@v1
        with:
          github-token: ${{ secrets.TEAM_SYNC_ADMIN_PAT }}
          config-path: .github/sync-config.json
          dry-run: true
~~~

Pinning `paladini/team-ai-sync@v1` follows the stable major-version example from the project documentation. For stricter supply-chain control, you can pin an immutable commit SHA and update it deliberately.

The `paths` filter prevents unrelated source changes from starting the job. Keeping `workflow_dispatch` is helpful while introducing the workflow because you can test configuration changes manually.

## Step 4: Run the dry run first

Commit the files, open the Actions tab, and run **Sync AI guidance**.

With `dry-run: true`, team-ai-sync calculates the work but does not push branches or create pull requests. Inspect the job log and confirm:

- every intended target is listed;
- every source resolves to the expected destination;
- no unrelated file is included;
- authentication works for all targets.

This is the most important rollout step. It separates configuration validation from mutation.

The action also exposes outputs including `changed`, `synced-targets`, `failed-targets`, and `pr-urls`. They are useful if you later want a summary step or a notification, but the first run should stay simple.

## Step 5: Enable reviewable synchronization

After the dry run is correct, remove this line:

~~~yaml
dry-run: true
~~~

Run the workflow again. For each repository with changes, the action creates or updates its synchronization branch and pull request. It does not merge the PR, approve it, bypass branch protection, or modify repository settings.

Review the generated diff exactly as you would review any other cross-repository policy change. The project includes a [public end-to-end demo pull request](https://github.com/paladini/team-ai-sync-demo-api/pull/1), so you can inspect the resulting workflow before enabling it in your own organization.

## Verification checklist

Before expanding to more repositories, verify one target end to end:

1. The workflow completes successfully.
2. The pull request contains only the configured files.
3. The destination paths are correct.
4. Branch protection still requires the expected reviews and checks.
5. A second run updates the existing PR instead of creating noisy duplicates.
6. A run with no source changes produces no unnecessary target change.

For implementation details, use the project's current [getting-started guide](https://github.com/paladini/team-ai-sync/blob/main/docs/getting-started.md), [authentication guide](https://github.com/paladini/team-ai-sync/blob/main/docs/authentication.md), and [security model](https://github.com/paladini/team-ai-sync/blob/main/docs/security.md). The action is also listed on the [GitHub Marketplace](https://github.com/marketplace/actions/team-ai-sync).

## Security boundaries and limitations

Centralization increases consistency, but it also increases the importance of the source repository and its credentials.

- Protect changes to the canonical files with code owners and required review.
- Scope tokens to the smallest possible repository set and permissions.
- Prefer a GitHub App for long-lived organization-wide automation.
- Never place secrets, credentials, or private model context in synchronized files.
- Start with one target and dry-run mode before broad rollout.
- Keep destructive orphan deletion disabled until its behavior is explicitly required and tested.

team-ai-sync supports GitHub Actions, GitLab CI/CD components, and a Bitbucket Pipe, but a single synchronization package targets repositories on the same platform. This tutorial intentionally covers GitHub only.

## Frequently asked questions

### Does team-ai-sync write directly to the default branch?

No. It creates or updates a branch and pull request. Your existing review rules, checks, and branch protection remain the approval boundary.

### Can I synchronize different files to different repositories?

Yes. Each target declares its own file mappings, so a backend service can receive a different subset from a frontend project.

### What happens when a canonical file changes again?

The next workflow run compares the new source content with each destination and updates the synchronization pull request when needed.

### Is this limited to `AGENTS.md`?

No. It works with repository files in general. AI agent instructions, prompts, editor rules, and supporting documentation are common use cases.

## A practical next step

Start with one low-risk instruction file and one target repository. Run the workflow in dry-run mode, inspect the plan, and only then enable pull-request creation. Once that path is trustworthy, add repositories incrementally.

The goal is not merely to copy files faster. It is to make shared AI guidance behave like maintained engineering configuration: versioned, reviewable, reproducible, and difficult to forget.

&amp;gt; Disclosure: This article was researched, fact-checked, and drafted with AI assistance using the current primary project sources linked above.

How does your team currently prevent AI agent instructions from drifting across repositories?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>ai</category>
      <category>githubactions</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>Stop Letting AI Agents Guess Hashes, UUIDs, and JWTs</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Tue, 28 Jul 2026 18:38:11 +0000</pubDate>
      <link>https://dev.to/paladini/stop-letting-ai-agents-guess-hashes-uuids-and-jwts-47l9</link>
      <guid>https://dev.to/paladini/stop-letting-ai-agents-guess-hashes-uuids-and-jwts-47l9</guid>
      <description>&lt;p&gt;AI coding agents are good at reasoning over messy requirements. They should not be improvising deterministic operations such as hashing a string, generating a UUID, decoding a JWT, formatting JSON, or calculating a CIDR range.&lt;/p&gt;

&lt;p&gt;Yet that is exactly what happens in many AI-assisted workflows. The model writes a throwaway script, reaches for a random website, produces an answer from memory, or spends several tool calls on a task that already has a precise implementation.&lt;/p&gt;

&lt;p&gt;I wanted those small operations to be boring again. So I built &lt;a href="https://github.com/paladini/devutils-mcp-server" rel="noopener noreferrer"&gt;DevUtils MCP Server&lt;/a&gt;: an open-source MCP server that exposes 36 local developer utilities to MCP-compatible AI assistants.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;DevUtils gives Cursor, Claude Desktop, Claude Code, VS Code, Windsurf, and other MCP clients a consistent set of tools for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hashing and encoding;&lt;/li&gt;
&lt;li&gt;UUID, Nano ID, password, and random-hex generation;&lt;/li&gt;
&lt;li&gt;JWT inspection;&lt;/li&gt;
&lt;li&gt;JSON formatting and querying;&lt;/li&gt;
&lt;li&gt;timestamp, number-base, color, and byte conversion;&lt;/li&gt;
&lt;li&gt;CIDR and IP operations;&lt;/li&gt;
&lt;li&gt;common text transformations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The operations run locally through an MCP stdio server and do not call external APIs. You can start it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; devutils-mcp-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Node.js 18 or newer is required.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real problem is not Base64
&lt;/h2&gt;

&lt;p&gt;Base64 encoding is easy. So is generating a UUID. That is why asking an AI model to handle these tasks feels harmless.&lt;/p&gt;

&lt;p&gt;But a long agent workflow may need dozens of small, exact transformations. Each improvised implementation adds friction and another place for subtle mistakes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a timestamp is interpreted in the wrong timezone;&lt;/li&gt;
&lt;li&gt;a JWT expiration is displayed as an unreadable integer;&lt;/li&gt;
&lt;li&gt;malformed JSON produces a vague error;&lt;/li&gt;
&lt;li&gt;a CIDR calculation uses the wrong host range;&lt;/li&gt;
&lt;li&gt;a model describes a hash instead of actually calculating it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are not reasoning problems. They are tool problems.&lt;/p&gt;

&lt;p&gt;MCP lets us give the agent explicit operations with named inputs, validated schemas, and structured results. The model decides &lt;em&gt;when&lt;/em&gt; to use a utility; the utility performs the deterministic work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install DevUtils in an MCP client
&lt;/h2&gt;

&lt;p&gt;For Cursor or Claude Desktop, add this server to the client's MCP configuration:&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;"devutils"&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;"devutils-mcp-server"&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;Claude Desktop stores this configuration in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;macOS: &lt;code&gt;~/Library/Application Support/Claude/claude_desktop_config.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Windows: &lt;code&gt;%APPDATA%\Claude\claude_desktop_config.json&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cursor can use the same server definition in &lt;code&gt;~/.cursor/mcp.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;VS Code uses a slightly different top-level key:&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;"servers"&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;"devutils"&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;"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;"stdio"&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;"devutils-mcp-server"&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;Restart or reload the client after changing its configuration. The exact UI varies by client, but you should see tools with names such as &lt;code&gt;generate_uuid&lt;/code&gt;, &lt;code&gt;jwt_decode&lt;/code&gt;, &lt;code&gt;json_validate&lt;/code&gt;, and &lt;code&gt;cidr_calculate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I also smoke-tested the currently published npm package directly: it completed the MCP initialization handshake, and &lt;code&gt;tools/list&lt;/code&gt; returned all 36 tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try four practical workflows
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Generate identifiers without writing a script
&lt;/h3&gt;

&lt;p&gt;Ask your assistant:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Generate three UUID v4 values and return them as a JSON array.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The agent can call &lt;code&gt;generate_uuid&lt;/code&gt; with batch support instead of inventing identifiers or creating a temporary program. For shorter URL-friendly identifiers, it can use &lt;code&gt;generate_nanoid&lt;/code&gt; with a configured length.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Inspect a JWT expiration date
&lt;/h3&gt;

&lt;p&gt;Ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Decode this JWT and explain its issued-at and expiration timestamps in UTC.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;DevUtils exposes &lt;code&gt;jwt_decode&lt;/code&gt; for the header and payload and returns human-readable dates. It also provides &lt;code&gt;jwt_validate&lt;/code&gt; for structure and expiration checks.&lt;/p&gt;

&lt;p&gt;There is an important boundary here: decoding a token is not the same as cryptographically verifying its signature. Do not treat a decoded payload as trusted identity data unless your application verifies the signature and expected claims with the correct key and algorithm.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Turn broken JSON into a useful error
&lt;/h3&gt;

&lt;p&gt;Ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Validate this JSON. If it is invalid, show me where the syntax fails. If it is valid, format it with two-space indentation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The agent can combine &lt;code&gt;json_validate&lt;/code&gt; and &lt;code&gt;json_format&lt;/code&gt;. For simple extraction, &lt;code&gt;json_path_query&lt;/code&gt; supports dot-notation paths, which is often enough to inspect an API response without bringing in a full query language.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Check a network range during debugging
&lt;/h3&gt;

&lt;p&gt;Ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Calculate the network address, broadcast address, mask, host range, and host count for &lt;code&gt;10.42.0.0/20&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That maps directly to &lt;code&gt;cidr_calculate&lt;/code&gt;. &lt;code&gt;ip_validate&lt;/code&gt; can also classify IPv4 and IPv6 input before another network operation uses it.&lt;/p&gt;

&lt;p&gt;These examples are intentionally ordinary. A utility server is valuable because it removes repeated micro-decisions from larger workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  What else is included?
&lt;/h2&gt;

&lt;p&gt;The 36 tools are grouped into eight areas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hashing:&lt;/strong&gt; MD5, SHA-1, SHA-256, SHA-512, bcrypt creation, and bcrypt verification.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encoding:&lt;/strong&gt; Base64, URL, HTML entity, and hexadecimal encoding and decoding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generators:&lt;/strong&gt; UUID v4, Nano ID, password, and random hexadecimal values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JWT:&lt;/strong&gt; payload decoding plus structure and expiration validation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON:&lt;/strong&gt; formatting, validation, and path queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Converters:&lt;/strong&gt; timestamps, numeric bases, colors, and byte units.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network:&lt;/strong&gt; CIDR calculation and IP validation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text:&lt;/strong&gt; statistics, case conversion, slug generation, regex tests, diffs, and placeholder text.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The server is implemented in TypeScript with the MCP SDK, Zod schemas, and stdio transport. The &lt;a href="https://github.com/paladini/devutils-mcp-server/blob/main/LICENSE" rel="noopener noreferrer"&gt;repository is available under the MIT license&lt;/a&gt;, and the package is &lt;a href="https://www.npmjs.com/package/devutils-mcp-server" rel="noopener noreferrer"&gt;published on npm as &lt;code&gt;devutils-mcp-server&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What “local” does and does not mean
&lt;/h2&gt;

&lt;p&gt;DevUtils performs its utility operations in the local MCP process. It does not send a hash input or JSON document to a separate conversion API.&lt;/p&gt;

&lt;p&gt;That does &lt;strong&gt;not&lt;/strong&gt; automatically make every prompt secret-safe. Your AI client and model may still receive the text you provide before deciding to call the tool. Do not paste production tokens, passwords, customer data, or private keys into an AI conversation merely because the final utility runs locally.&lt;/p&gt;

&lt;p&gt;The usual cryptographic rules also still apply. MD5 and SHA-1 remain available for compatibility and checksums, not for secure password storage. Use an appropriate password-hashing design when protecting credentials.&lt;/p&gt;

&lt;h2&gt;
  
  
  When should you not use it?
&lt;/h2&gt;

&lt;p&gt;If you are writing application code, use the standard library or a focused dependency. A direct &lt;code&gt;crypto&lt;/code&gt; or &lt;code&gt;hashlib&lt;/code&gt; call is faster and easier to test inside the application than routing the operation through MCP.&lt;/p&gt;

&lt;p&gt;DevUtils is aimed at AI-agent workflows: debugging, investigation, repository maintenance, data inspection, and other multi-step tasks where the assistant benefits from reliable utilities.&lt;/p&gt;

&lt;p&gt;It is also not useful if your client does not support MCP, and it adds process and protocol overhead compared with a direct function call. The point is consistency and tool availability, not maximum throughput.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does DevUtils MCP Server require an API key?
&lt;/h3&gt;

&lt;p&gt;No. The server's 36 utility operations do not require external API credentials. Installing the npm package initially requires access to the npm registry unless it is already cached.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does it work only with one AI assistant?
&lt;/h3&gt;

&lt;p&gt;No. It uses MCP over stdio, so it can work with clients that support that transport. The repository documents configurations for Cursor, Claude Desktop, Claude Code, VS Code, Windsurf, and Docker-based setups.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can it verify that a JWT is authentic?
&lt;/h3&gt;

&lt;p&gt;No. The JWT tools decode tokens and validate their structure and expiration. Authenticity requires cryptographic signature verification with the correct key, algorithm, issuer, audience, and application policy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Are all operations performed without external utility APIs?
&lt;/h3&gt;

&lt;p&gt;Yes. The transformations themselves run in the local server process. Your AI client's own data handling remains a separate concern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make deterministic work deterministic
&lt;/h2&gt;

&lt;p&gt;AI agents should spend their reasoning budget on the parts of development that actually require reasoning.&lt;/p&gt;

&lt;p&gt;If your assistant repeatedly writes one-off scripts for hashes, IDs, encodings, timestamps, JSON, or network calculations, try &lt;a href="https://github.com/paladini/devutils-mcp-server" rel="noopener noreferrer"&gt;DevUtils MCP Server on GitHub&lt;/a&gt;. Test it on a real workflow, inspect the tool calls, and open a discussion if a small developer utility is still missing.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Host a Free Ghost Blog on GitHub Pages (Without Paying for a Server)</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Tue, 28 Jul 2026 14:42:31 +0000</pubDate>
      <link>https://dev.to/paladini/host-a-free-ghost-blog-on-github-pages-without-paying-for-a-server-1n6i</link>
      <guid>https://dev.to/paladini/host-a-free-ghost-blog-on-github-pages-without-paying-for-a-server-1n6i</guid>
      <description>&lt;p&gt;Ghost is one of the nicest writing experiences in blogging. The catch? A normal Ghost install expects a server, a database, and usually a hosting bill.&lt;/p&gt;

&lt;p&gt;GitHub Pages is free and reliable — but it only serves &lt;strong&gt;static files&lt;/strong&gt;. It will not run Ghost for you.&lt;/p&gt;

&lt;p&gt;So the real question is not “Can Ghost run on GitHub Pages?”&lt;/p&gt;

&lt;p&gt;It is: &lt;strong&gt;Can I write in Ghost locally, then publish a static copy of my blog for free?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. And after years of people hitting broken images, &lt;code&gt;localhost&lt;/code&gt; links on the live site, and Python 2 install failures, that workflow finally got a proper refresh.&lt;/p&gt;

&lt;p&gt;This post walks through the problem, the approach, and how to set it up with &lt;strong&gt;&lt;a href="https://github.com/paladini/ghost-on-github-pages" rel="noopener noreferrer"&gt;Ghost on GitHub Pages&lt;/a&gt; v3.0.0&lt;/strong&gt; — a major release that replaces the old brittle path with something you can actually maintain in 2026.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem in plain terms
&lt;/h2&gt;

&lt;p&gt;You probably want some mix of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ghost’s editor and admin UI&lt;/li&gt;
&lt;li&gt;A public blog URL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;$0/month&lt;/strong&gt; hosting&lt;/li&gt;
&lt;li&gt;No Kubernetes, no managed DB, no “just spin up a droplet”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GitHub Pages is perfect for the hosting part. Ghost is perfect for the writing part. They do not speak the same language out of the box.&lt;/p&gt;

&lt;p&gt;The workable pattern has always been:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run Ghost on your machine (&lt;code&gt;localhost&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Generate a &lt;strong&gt;static HTML&lt;/strong&gt; snapshot of the site&lt;/li&gt;
&lt;li&gt;Push that snapshot to a GitHub Pages repo&lt;/li&gt;
&lt;li&gt;Repeat whenever you publish a new post&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That idea is solid. The tooling around it was the painful part.&lt;/p&gt;

&lt;h3&gt;
  
  
  What used to go wrong
&lt;/h3&gt;

&lt;p&gt;If you tried this years ago (or inherited an old setup), you may remember:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python 2 / buster dependency hell&lt;/strong&gt; on modern machines&lt;/li&gt;
&lt;li&gt;Live pages whose nav or tags pointed to &lt;code&gt;http://localhost:2368&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broken images&lt;/strong&gt; after publish&lt;/li&gt;
&lt;li&gt;Scripts that published too much (or hung for confusing reasons)&lt;/li&gt;
&lt;li&gt;Docs that assumed you already knew what a “static generator” was&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those were not niche complaints. They showed up repeatedly as real issues — and they are exactly what v3 targets.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Ghost on GitHub Pages actually does
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/paladini/ghost-on-github-pages" rel="noopener noreferrer"&gt;Ghost on GitHub Pages&lt;/a&gt; is a small open-source toolkit that wires that local-Ghost → static-site → GitHub Pages loop together for you.&lt;/p&gt;

&lt;p&gt;At a high level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Edit in Ghost (localhost:2373)
        ↓
gssg builds static HTML/CSS/assets
        ↓
Push to your GitHub repo
        ↓
Live blog on GitHub Pages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;You keep writing in Ghost.&lt;/strong&gt; Visitors get a fast static site. GitHub hosts it for free.&lt;/p&gt;

&lt;h3&gt;
  
  
  What’s new in v3.0.0
&lt;/h3&gt;

&lt;p&gt;Version 3 is a real major update after a long stretch:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before (Classic v2)&lt;/th&gt;
&lt;th&gt;Now (v3)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Python 2 + buster&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Node.js + wget&lt;/strong&gt; only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fragile link/image rewrite&lt;/td&gt;
&lt;td&gt;Publishing via &lt;strong&gt;&lt;a href="https://github.com/StefanNedelcu/ghost-static-site-generator" rel="noopener noreferrer"&gt;gssg&lt;/a&gt;&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual / risky upgrades&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;&lt;code&gt;./migrate.sh&lt;/code&gt;&lt;/strong&gt; with automatic backup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sparse docs&lt;/td&gt;
&lt;td&gt;Plain-English guides in &lt;code&gt;docs/&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It also addresses long-standing bugs around localhost links, image paths, install errors, and publishing the wrong set of files. Details live in the &lt;a href="https://github.com/paladini/ghost-on-github-pages/blob/master/CHANGELOG.md" rel="noopener noreferrer"&gt;changelog&lt;/a&gt; and &lt;a href="https://github.com/paladini/ghost-on-github-pages/blob/master/RELEASE_NOTES_v3.0.0.md" rel="noopener noreferrer"&gt;v3 release notes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live demos:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://paladini.github.io/ghost-on-github-pages-demo/" rel="noopener noreferrer"&gt;v3 demo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://paladini.github.io/ghost-on-github-pages-demo/legacy/v2/static/" rel="noopener noreferrer"&gt;Classic v2 archive demo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Who this is for (and who it is not)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Good fit if you:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Want Ghost’s writing UX without paying for Ghost(Pro) or a VPS&lt;/li&gt;
&lt;li&gt;Are fine editing locally and running a deploy script when ready&lt;/li&gt;
&lt;li&gt;Prefer GitHub Pages (or can adapt the same static output elsewhere)&lt;/li&gt;
&lt;li&gt;Use macOS, Linux, or Windows via WSL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Not a great fit if you need:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live server-side Ghost features on the public site (members, dynamic search, server-rendered previews, etc.)&lt;/li&gt;
&lt;li&gt;Instant publish from any device without touching your machine&lt;/li&gt;
&lt;li&gt;A fully managed “click publish and forget” SaaS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Be honest with yourself here: this is &lt;strong&gt;static publishing with Ghost as the CMS&lt;/strong&gt;, not “Ghost Cloud for free.”&lt;/p&gt;




&lt;h2&gt;
  
  
  Tutorial: set up a free Ghost blog on GitHub Pages
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;Before installing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Node.js LTS&lt;/strong&gt; (v18 or v20) — &lt;a href="https://nodejs.org/" rel="noopener noreferrer"&gt;nodejs.org&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;wget&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;A free &lt;strong&gt;GitHub&lt;/strong&gt; account and a &lt;strong&gt;public&lt;/strong&gt; repository for the blog&lt;/li&gt;
&lt;li&gt;Internet access for the initial Ghost download&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On macOS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;wget
node &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Ubuntu/Debian:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; wget
node &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create an empty public GitHub repo (for example &lt;code&gt;my-blog&lt;/code&gt;) and keep the clone URL handy.&lt;/p&gt;

&lt;p&gt;Full checklist: &lt;a href="https://github.com/paladini/ghost-on-github-pages/blob/master/docs/v3/REQUIREMENTS.md" rel="noopener noreferrer"&gt;Requirements&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 — Download v3
&lt;/h3&gt;

&lt;p&gt;Grab the latest release:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/paladini/ghost-on-github-pages/releases/latest" rel="noopener noreferrer"&gt;github.com/paladini/ghost-on-github-pages/releases/latest&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Extract it somewhere memorable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2 — Install
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;path/to/ghost-on-github-pages
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x install.sh
./install.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This takes a few minutes. It installs Ghost locally. Your blog folder ends up at &lt;code&gt;~/.ghost&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When prompted, you can publish to GitHub immediately or skip and do it later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3 — Configure Ghost
&lt;/h3&gt;

&lt;p&gt;Open:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;http://localhost:2373/ghost&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Create your admin account, write a test post, poke around the editor.&lt;/p&gt;

&lt;p&gt;Local preview of the site:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;http://localhost:2373&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4 — Publish to GitHub Pages
&lt;/h3&gt;

&lt;p&gt;If you skipped deploy during install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/.ghost
./deploy.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script asks for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub username&lt;/li&gt;
&lt;li&gt;Repository name&lt;/li&gt;
&lt;li&gt;Repository URL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Wait ~10 minutes for Pages to catch up, then open:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://YOUR_USERNAME.github.io/YOUR_REPO&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Sanity checklist
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Local blog loads at &lt;code&gt;http://localhost:2373&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;./deploy.sh&lt;/code&gt; finishes cleanly&lt;/li&gt;
&lt;li&gt;[ ] The GitHub repo has new static files&lt;/li&gt;
&lt;li&gt;[ ] The public URL loads&lt;/li&gt;
&lt;li&gt;[ ] Post/tag links do &lt;strong&gt;not&lt;/strong&gt; point at localhost&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If something fails, start here: &lt;a href="https://github.com/paladini/ghost-on-github-pages/blob/master/docs/TROUBLESHOOTING.md" rel="noopener noreferrer"&gt;Troubleshooting&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Updating later
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Edit at &lt;code&gt;http://localhost:2373/ghost&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Publish again:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/.ghost
./deploy.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole loop.&lt;/p&gt;




&lt;h2&gt;
  
  
  How publishing works under the hood
&lt;/h2&gt;

&lt;p&gt;When you run &lt;code&gt;deploy.sh&lt;/code&gt;, the flow is roughly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ensure Ghost is available locally&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;gssg&lt;/strong&gt; to crawl/generate a static copy of the site&lt;/li&gt;
&lt;li&gt;Rewrite URLs so the public site uses your GitHub Pages domain — not &lt;code&gt;localhost&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Push only the static output to your configured repo&lt;/li&gt;
&lt;li&gt;GitHub Pages serves those files&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;v3 also includes helpers like &lt;code&gt;scripts/validate-static.sh&lt;/code&gt; so you can catch broken links/images &lt;strong&gt;before&lt;/strong&gt; visitors do, plus optional multi-site deploy profiles (&lt;code&gt;deploy.sh --site&lt;/code&gt; / &lt;code&gt;sites.conf&lt;/code&gt;) if you maintain more than one blog.&lt;/p&gt;

&lt;p&gt;Saved deploy settings live in &lt;code&gt;~/.ghost/deploy.conf&lt;/code&gt;, so you are not retyping repo details every time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Features worth knowing about
&lt;/h2&gt;

&lt;p&gt;Beyond “install and deploy,” v3 ships with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Simpler dependency story&lt;/strong&gt; — no Python 2&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More reliable static output&lt;/strong&gt; — fewer broken images / localhost leaks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration path from Classic v2&lt;/strong&gt; — &lt;code&gt;./migrate.sh&lt;/code&gt; with automatic backup&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plain-English documentation&lt;/strong&gt; — getting started, deploy, glossary, troubleshooting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static validation script&lt;/strong&gt; — catch bad links/images pre-publish&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple GitHub repos / site profiles&lt;/strong&gt; — useful if you run more than one site&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI in the project itself&lt;/strong&gt; — shellcheck + validation tests on the toolkit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are on Classic v2 and it still publishes fine, you can stay. Migrate when you hit Python errors, broken assets, localhost links, or you are setting up a new machine and do not want to resurrect Python 2.&lt;/p&gt;

&lt;p&gt;Migration guide: &lt;a href="https://github.com/paladini/ghost-on-github-pages/blob/master/docs/MIGRATION.md" rel="noopener noreferrer"&gt;docs/MIGRATION.md&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  A note on expectations
&lt;/h2&gt;

&lt;p&gt;This project will not magically turn GitHub Pages into a full Ghost server. Comments widgets, membership, newsletters, and other dynamic Ghost features need a real Ghost host or third-party services.&lt;/p&gt;

&lt;p&gt;What it &lt;em&gt;does&lt;/em&gt; well is the thing many indie writers and side-project blogs actually need:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A pleasant editor locally + a free, fast, public static blog.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If that is your goal, the v3 path is substantially less painful than the Classic era.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/paladini/ghost-on-github-pages" rel="noopener noreferrer"&gt;paladini/ghost-on-github-pages&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Latest release: &lt;a href="https://github.com/paladini/ghost-on-github-pages/releases/latest" rel="noopener noreferrer"&gt;v3.0.0 / latest&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Getting started: &lt;a href="https://github.com/paladini/ghost-on-github-pages/blob/master/docs/v3/GETTING-STARTED.md" rel="noopener noreferrer"&gt;docs/v3/GETTING-STARTED.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Migration from Classic: &lt;a href="https://github.com/paladini/ghost-on-github-pages/blob/master/docs/MIGRATION.md" rel="noopener noreferrer"&gt;docs/MIGRATION.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;v3 demo: &lt;a href="https://paladini.github.io/ghost-on-github-pages-demo/" rel="noopener noreferrer"&gt;paladini.github.io/ghost-on-github-pages-demo&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Questions and bug reports welcome via &lt;a href="https://github.com/paladini/ghost-on-github-pages/issues/new" rel="noopener noreferrer"&gt;GitHub Issues&lt;/a&gt;. Docs contributions count too — you do not need to be a developer to help.&lt;/p&gt;

&lt;p&gt;If this saved you a hosting bill (or an afternoon fighting Python 2), a star on the repo helps other people find it.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Measure Your AI Coding Harness Maturity in CI with GitHub Actions</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Mon, 27 Jul 2026 11:57:05 +0000</pubDate>
      <link>https://dev.to/paladini/measure-your-ai-coding-harness-maturity-in-ci-with-github-actions-3k4m</link>
      <guid>https://dev.to/paladini/measure-your-ai-coding-harness-maturity-in-ci-with-github-actions-3k4m</guid>
      <description>&lt;p&gt;Two teams can use the same model and the same prompt — and get completely different results.&lt;/p&gt;

&lt;p&gt;The difference is often not the model. It is the &lt;strong&gt;harness&lt;/strong&gt;: the guides, rules, skills, hooks, tests, and CI feedback wrapped around the agent.&lt;/p&gt;

&lt;p&gt;Most repositories build that harness slowly. Almost none of them &lt;strong&gt;measure&lt;/strong&gt; it. Worse: a pull request can quietly delete &lt;code&gt;hooks.json&lt;/code&gt; or drop a CI gate, and nobody notices until an agent does something expensive.&lt;/p&gt;

&lt;p&gt;This tutorial shows how to put a deterministic maturity score into your GitHub Actions pipeline with &lt;a href="https://github.com/paladini/harness-score" rel="noopener noreferrer"&gt;Harness Score&lt;/a&gt; — so AI-assisted repos get the same kind of ratchet you already expect from tests and lint.&lt;/p&gt;

&lt;h2&gt;
  
  
  What “AI harness maturity” means
&lt;/h2&gt;

&lt;p&gt;Harness Score scans filesystem evidence across tools like Cursor, Claude Code, Windsurf, Cline, Continue, Codex, and Copilot. It returns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a maturity level from &lt;strong&gt;L0 (Unharnessed)&lt;/strong&gt; to &lt;strong&gt;L4 (Self-correcting)&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;a score across &lt;strong&gt;six dimensions&lt;/strong&gt; (up to 108 points)&lt;/li&gt;
&lt;li&gt;a ranked list of what to fix next&lt;/li&gt;
&lt;/ul&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;Name&lt;/th&gt;
&lt;th&gt;Rough meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;L0&lt;/td&gt;
&lt;td&gt;Unharnessed&lt;/td&gt;
&lt;td&gt;Agents rediscover the project every session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L1&lt;/td&gt;
&lt;td&gt;Documented&lt;/td&gt;
&lt;td&gt;Substantive context file exists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L2&lt;/td&gt;
&lt;td&gt;Guided&lt;/td&gt;
&lt;td&gt;Scoped rules / skills + basic hygiene&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L3&lt;/td&gt;
&lt;td&gt;Sensing&lt;/td&gt;
&lt;td&gt;Tests, lint, types, and CI feedback exist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L4&lt;/td&gt;
&lt;td&gt;Self-correcting&lt;/td&gt;
&lt;td&gt;Gate + feedback hooks close the loop&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Important constraints (by design):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No LLM calls&lt;/strong&gt; during the scan&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No network&lt;/strong&gt; during the scan&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Same commit ⇒ same score&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is what makes the number safe to gate CI on.&lt;/p&gt;

&lt;p&gt;Try it locally first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx harness-score@1.5.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why put this in GitHub Actions
&lt;/h2&gt;

&lt;p&gt;Local scans are useful. CI is the contract.&lt;/p&gt;

&lt;p&gt;With the Action in your pipeline you can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fail PRs&lt;/strong&gt; that drop below a minimum maturity level&lt;/li&gt;
&lt;li&gt;Show a &lt;strong&gt;job summary&lt;/strong&gt; with the dimension breakdown&lt;/li&gt;
&lt;li&gt;Post a &lt;strong&gt;sticky PR comment&lt;/strong&gt; with score deltas vs the base branch&lt;/li&gt;
&lt;li&gt;Emit a &lt;strong&gt;README badge&lt;/strong&gt; that updates when maturity changes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Official Action: &lt;a href="https://github.com/marketplace/actions/harness-score" rel="noopener noreferrer"&gt;Harness Score on the GitHub Marketplace&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Tutorial: add Harness Score to CI
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) Create the workflow
&lt;/h3&gt;

&lt;p&gt;Add &lt;code&gt;.github/workflows/harness.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Harness maturity&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;harness&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;paladini/harness-score@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;min-level&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0'&lt;/span&gt;              &lt;span class="c1"&gt;# report only for now&lt;/span&gt;
          &lt;span class="na"&gt;badge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;harness-badge.svg'&lt;/span&gt;
          &lt;span class="na"&gt;report&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;harness-report.md'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commit, push, and open the Actions tab. You should see a job summary like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Harness Score: L2 · Guided (65% maturity)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;plus a per-dimension table.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Tip: for stronger supply-chain hygiene, pin the Action to a full commit SHA instead of &lt;code&gt;@v1&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  2) Start gating when you know your baseline
&lt;/h3&gt;

&lt;p&gt;Once you know where the repo sits, raise the floor.&lt;/p&gt;

&lt;p&gt;Example: require at least &lt;strong&gt;L3 · Sensing&lt;/strong&gt; on &lt;code&gt;main&lt;/code&gt; and PRs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;paladini/harness-score@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;min-level&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3'&lt;/span&gt;
    &lt;span class="na"&gt;badge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;harness-badge.svg'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If maturity falls below L3, the job fails and prints the gaps needed to recover (for example: missing sensors or CI coverage).&lt;/p&gt;

&lt;p&gt;A practical rollout:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Week 1: &lt;code&gt;min-level: '0'&lt;/code&gt; (observe)&lt;/li&gt;
&lt;li&gt;Week 2: set &lt;code&gt;min-level&lt;/code&gt; to your &lt;strong&gt;current&lt;/strong&gt; level (prevent regressions)&lt;/li&gt;
&lt;li&gt;Later: bump one level at a time as you add guides, sensors, and hooks&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  3) Add sticky PR comments (optional, highly recommended)
&lt;/h3&gt;

&lt;p&gt;On &lt;code&gt;pull_request&lt;/code&gt; events, the Action can compare the PR head against the base branch and update a single sticky comment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;harness&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;harness-score-${{ github.event.pull_request.number }}&lt;/span&gt;
      &lt;span class="na"&gt;cancel-in-progress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;paladini/harness-score@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;min-level&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3'&lt;/span&gt;
          &lt;span class="na"&gt;comment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;
          &lt;span class="na"&gt;badge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;harness-badge.svg'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You need &lt;code&gt;pull-requests: write&lt;/code&gt; — the Action cannot grant that for you.&lt;/p&gt;

&lt;p&gt;The comment shows level movement (for example &lt;code&gt;L2 → L3&lt;/code&gt;), score deltas, and newly passing / newly failing checks. That makes harness regressions visible in review, not only in a red X on the Checks tab.&lt;/p&gt;

&lt;h3&gt;
  
  
  4) Publish the badge in your README
&lt;/h3&gt;

&lt;p&gt;The Action can write &lt;code&gt;harness-badge.svg&lt;/code&gt; on every run. A common pattern is committing it to a &lt;code&gt;badges&lt;/code&gt; branch (or uploading it wherever you host static assets), then embedding:&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="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Harness Score"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://raw.githubusercontent.com/&amp;lt;you&amp;gt;&lt;/span&gt;/&lt;span class="s"&gt;&amp;lt;repo&amp;gt;&lt;/span&gt;/badges/harness-badge.svg" height="20"&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also pin a static level badge from the docs site if you prefer not to auto-update.&lt;/p&gt;

&lt;p&gt;More badge recipes: &lt;a href="https://paladini.github.io/harness-score/guide/measure-and-improve#show-your-maturity" rel="noopener noreferrer"&gt;Show your maturity&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Useful Action inputs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;min-level&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fail when maturity is below this level (0–4)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;badge&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;harness-badge.svg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SVG pill path (&lt;code&gt;empty&lt;/code&gt; to skip)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;report&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;(empty)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;Write a Markdown report&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;comment&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sticky PR comment with score delta&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;working-directory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Subdirectory to scan (monorepos)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pinned release&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;harness-score&lt;/code&gt; npm version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;maturity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Gate on &lt;code&gt;maturity&lt;/code&gt; (repo-only) or &lt;code&gt;effective&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;include-user-harness&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Include user-level harness paths in effective score&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;config&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.harness-score.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Custom config path&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Outputs you can consume in later steps: &lt;code&gt;level&lt;/code&gt;, &lt;code&gt;level-name&lt;/code&gt;, &lt;code&gt;percent&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optional: repository config
&lt;/h2&gt;

&lt;p&gt;If you need team policy (scopes, presets, per-check rules), add &lt;code&gt;.harness-score.json&lt;/code&gt; at the repo root.&lt;/p&gt;

&lt;p&gt;Example: keep CI gated on &lt;strong&gt;repository&lt;/strong&gt; maturity, while still allowing local diagnosis with user-scope overlays:&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;"scopes"&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;"user"&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;"system"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;"gate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"maturity"&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;Customization is transparent: excluded checks are removed from both earned and available points (no free credit), and security checks for exposed credentials cannot be disabled.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does &lt;em&gt;not&lt;/em&gt; claim
&lt;/h2&gt;

&lt;p&gt;A high score means the &lt;strong&gt;infrastructure&lt;/strong&gt; for reliable agent work exists:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;context and rules&lt;/li&gt;
&lt;li&gt;skills/commands&lt;/li&gt;
&lt;li&gt;hooks/guardrails&lt;/li&gt;
&lt;li&gt;sensors (tests/lint/types)&lt;/li&gt;
&lt;li&gt;CI feedback&lt;/li&gt;
&lt;li&gt;hygiene/safety&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt; mean your tests are good, your rules are true, or every runtime path honors the committed harness. That honesty is intentional — deterministic scanners should not pretend to judge vibes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full starter workflow
&lt;/h2&gt;

&lt;p&gt;Copy-paste version with gate + PR comment + badge + report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Harness maturity&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;harness&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;harness-score-${{ github.event.pull_request.number || github.ref }}&lt;/span&gt;
      &lt;span class="na"&gt;cancel-in-progress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Scan AI harness maturity&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;harness&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;paladini/harness-score@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;min-level&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3'&lt;/span&gt;
          &lt;span class="na"&gt;comment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;
          &lt;span class="na"&gt;badge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;harness-badge.svg'&lt;/span&gt;
          &lt;span class="na"&gt;report&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;harness-report.md'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload report artifacts&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;harness-score&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;harness-badge.svg&lt;/span&gt;
            &lt;span class="s"&gt;harness-report.md&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Try it, then ratchet
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;npx harness-score@1.5.1&lt;/code&gt; locally&lt;/li&gt;
&lt;li&gt;Add the workflow with &lt;code&gt;min-level: '0'&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;min-level&lt;/code&gt; to your current level to prevent silent regressions&lt;/li&gt;
&lt;li&gt;Climb toward L3/L4 with the ranked fix list from the report&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Links:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/paladini/harness-score" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/marketplace/actions/harness-score" rel="noopener noreferrer"&gt;GitHub Marketplace Action&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://paladini.github.io/harness-score/" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://paladini.github.io/harness-score/guide/measure-and-improve" rel="noopener noreferrer"&gt;Measure &amp;amp; Improve guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you add this to a repo, comment with your starting level (L0–L4) and the first check that surprised you — those reports are the best way to harden the maturity model.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>devops</category>
      <category>github</category>
    </item>
    <item>
      <title>I Built an Ad-Free Open-Source Prep Pack for Wonderlic Select (and Other Hiring Cognitive Tests)</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Sun, 26 Jul 2026 17:55:14 +0000</pubDate>
      <link>https://dev.to/paladini/i-built-an-ad-free-open-source-prep-pack-for-wonderlic-select-and-other-hiring-cognitive-tests-1g6b</link>
      <guid>https://dev.to/paladini/i-built-an-ad-free-open-source-prep-pack-for-wonderlic-select-and-other-hiring-cognitive-tests-1g6b</guid>
      <description>&lt;p&gt;If you have applied for a job lately — in tech or outside it — there is a decent chance you were asked to take a &lt;strong&gt;Wonderlic&lt;/strong&gt; assessment. Maybe it was the classic &lt;strong&gt;50 multiple-choice questions in 12 minutes&lt;/strong&gt;. Maybe it was &lt;strong&gt;Wonderlic Select&lt;/strong&gt;, which layers cognitive ability with personality and motivation sections and rolls them into a single job-fit score.&lt;/p&gt;

&lt;p&gt;Either way, the format is brutal: roughly &lt;strong&gt;14.4 seconds per question&lt;/strong&gt;, no calculator, easy material under time pressure. The test is not really about whether you know calculus. It is about whether you can move fast, skip wisely, and stay calm when the clock is running.&lt;/p&gt;

&lt;p&gt;And then you search for prep material online.&lt;/p&gt;

&lt;p&gt;Most of what you find is a funnel: a few free sample questions, a paywall, ads, and upsells. That is fine if you want to buy a course. It is not fine if you just need honest practice before a screening round that might decide whether you get to the next interview.&lt;/p&gt;

&lt;p&gt;So I built something different.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem is not just "study harder"
&lt;/h2&gt;

&lt;p&gt;The Wonderlic has been used in hiring since 1936. You will see it in tech companies, healthcare, logistics, finance, and plenty of roles where employers want a short cognitive screen before investing more interview time.&lt;/p&gt;

&lt;p&gt;The classic format — &lt;strong&gt;WPT-R&lt;/strong&gt; — is 50 questions in 12 minutes. There are shorter variants like &lt;strong&gt;WPT-Q&lt;/strong&gt; (30 questions / 8 minutes). Schools and nursing programs often use the &lt;strong&gt;SLE&lt;/strong&gt;. Entry-level roles sometimes hit the &lt;strong&gt;WBST&lt;/strong&gt; for basic verbal and math skills.&lt;/p&gt;

&lt;p&gt;Then there is &lt;strong&gt;Wonderlic Select&lt;/strong&gt; (formerly WonScore). That one is especially common in modern hiring pipelines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~50 cognitive questions (same family as WPT-R)&lt;/li&gt;
&lt;li&gt;Personality section (Big Five traits)&lt;/li&gt;
&lt;li&gt;Motivation section (RIASEC / Holland codes)&lt;/li&gt;
&lt;li&gt;Roughly &lt;strong&gt;35 minutes total&lt;/strong&gt;, with a combined job-fit score at the end&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For software engineers, the cognitive bar is often higher — many employers expect a &lt;strong&gt;30+&lt;/strong&gt; on the 0–50 scale — and Select adds a twist people do not expect: &lt;strong&gt;you cannot pass on cognitive ability alone&lt;/strong&gt;. The personality and motivation sections matter too.&lt;/p&gt;

&lt;p&gt;That combination catches candidates off guard. You might be strong at LeetCode and still stumble because you never practiced timed verbal analogies, mental math without a calculator, or the "answer honestly but strategically" mindset Select expects on the non-cognitive sections.&lt;/p&gt;

&lt;p&gt;Most free resources online do not help much. They are either generic IQ trivia, outdated blog posts, or lead-gen pages dressed up as prep sites.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I shipped: Awesome Wonderlic
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/paladini/awesome-wonderlic" rel="noopener noreferrer"&gt;Awesome Wonderlic&lt;/a&gt;&lt;/strong&gt; is a curated, ad-free, open-source prep pack for Wonderlic-style hiring assessments.&lt;/p&gt;

&lt;p&gt;Everything in the repo is genuinely free:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No ad-funded prep sites&lt;/li&gt;
&lt;li&gt;No freemium funnels&lt;/li&gt;
&lt;li&gt;No paid plans hiding behind a login wall&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is a &lt;strong&gt;dataset + curriculum + tooling&lt;/strong&gt; project — not another timed quiz SaaS.&lt;/p&gt;

&lt;p&gt;Content is dual-licensed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CC BY-SA 4.0&lt;/strong&gt; for docs, questions, and practice tests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MIT&lt;/strong&gt; for scripts and CI&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is inside the repo
&lt;/h2&gt;

&lt;h3&gt;
  
  
  150 original practice questions
&lt;/h3&gt;

&lt;p&gt;The question bank has &lt;strong&gt;150 original Wonderlic-style items&lt;/strong&gt; across all six documented categories:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Verbal reasoning&lt;/li&gt;
&lt;li&gt;Vocabulary&lt;/li&gt;
&lt;li&gt;Arithmetic&lt;/li&gt;
&lt;li&gt;Word problems&lt;/li&gt;
&lt;li&gt;Logic and patterns&lt;/li&gt;
&lt;li&gt;General knowledge&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each question lives in structured JSON with an answer key, difficulty rating (1–5), and a full explanation. The bank is validated in CI against a JSON Schema, so contributions stay consistent.&lt;/p&gt;

&lt;p&gt;Exports are auto-generated to &lt;strong&gt;CSV&lt;/strong&gt;, &lt;strong&gt;Markdown&lt;/strong&gt;, and &lt;strong&gt;Anki TSV&lt;/strong&gt; if you want flashcard drilling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Full timed practice tests
&lt;/h3&gt;

&lt;p&gt;There are two ready-to-run &lt;strong&gt;50-question / 12-minute&lt;/strong&gt; simulations with realistic category mix and difficulty curve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/paladini/awesome-wonderlic/blob/main/practice-tests/practice-test-1.md" rel="noopener noreferrer"&gt;&lt;code&gt;practice-tests/practice-test-1.md&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/paladini/awesome-wonderlic/blob/main/practice-tests/practice-test-2.md" rel="noopener noreferrer"&gt;&lt;code&gt;practice-tests/practice-test-2.md&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Simulate real conditions: 12 minutes on a timer, no calculator, scratch paper only, no jumping around the test.&lt;/p&gt;

&lt;p&gt;Need more? A Python script assembles unlimited randomized tests from the bank:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; scripts/requirements.txt
python scripts/build_practice_test.py &lt;span class="nt"&gt;--seed&lt;/span&gt; 7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Study guides and plans
&lt;/h3&gt;

&lt;p&gt;The docs walk you through the parts most prep sites skip:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/paladini/awesome-wonderlic/blob/main/docs/test-types.md" rel="noopener noreferrer"&gt;Test types explained&lt;/a&gt; — WPT-R, WPT-Q, Select, SLE, WBST&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/paladini/awesome-wonderlic/blob/main/docs/scoring.md" rel="noopener noreferrer"&gt;Scoring explained&lt;/a&gt; — the 0–50 scale, percentile intuition, role-based targets&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/paladini/awesome-wonderlic/blob/main/docs/question-categories.md" rel="noopener noreferrer"&gt;Question categories&lt;/a&gt; — the six families with worked examples&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/paladini/awesome-wonderlic/blob/main/docs/study-guide.md" rel="noopener noreferrer"&gt;Study guide and strategy&lt;/a&gt; — pacing, skip strategy, no-calculator tricks, error logs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/paladini/awesome-wonderlic/blob/main/docs/test-day-tips.md" rel="noopener noreferrer"&gt;Test-day tips&lt;/a&gt; — a practical checklist&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Structured study plans:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/paladini/awesome-wonderlic/blob/main/docs/study-plans/7-day-plan.md" rel="noopener noreferrer"&gt;7-day crash course&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/paladini/awesome-wonderlic/blob/main/docs/study-plans/14-day-plan.md" rel="noopener noreferrer"&gt;14-day balanced plan&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/paladini/awesome-wonderlic/blob/main/docs/study-plans/30-day-plan.md" rel="noopener noreferrer"&gt;30-day thorough plan&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For engineers facing Select specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/paladini/awesome-wonderlic/blob/main/docs/wonderlic-select-software-developers.md" rel="noopener noreferrer"&gt;Wonderlic Select for software developers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/paladini/awesome-wonderlic/blob/main/docs/study-plans/select-software-developer-14-day-plan.md" rel="noopener noreferrer"&gt;14-day Select plan for software developers&lt;/a&gt; (plus a compressed 5-day variant)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One line from that guide worth repeating: &lt;strong&gt;the Wonderlic is not a knowledge test — it is a speed test over easy material.&lt;/strong&gt; Select adds: &lt;strong&gt;you cannot pass on cognitive ability alone.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Curated external resources
&lt;/h3&gt;

&lt;p&gt;The README also links vetted free material: official Wonderlic candidate FAQs and sample PDFs, the ICAR public cognitive item bank, open-source cognitive drill tools, and academic papers. The curation policy is strict — commercial prep sites are out of scope even when they offer free samples.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start here (5 steps)
&lt;/h2&gt;

&lt;p&gt;If the test is coming up, this is the fastest path in the repo:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read &lt;a href="https://github.com/paladini/awesome-wonderlic/blob/main/docs/test-types.md" rel="noopener noreferrer"&gt;What is the Wonderlic?&lt;/a&gt; and identify which variant you are facing.&lt;/li&gt;
&lt;li&gt;Read &lt;a href="https://github.com/paladini/awesome-wonderlic/blob/main/docs/scoring.md" rel="noopener noreferrer"&gt;How scoring works&lt;/a&gt; and set a realistic target for your role.&lt;/li&gt;
&lt;li&gt;Skim the &lt;a href="https://github.com/paladini/awesome-wonderlic/blob/main/docs/question-categories.md" rel="noopener noreferrer"&gt;six question categories&lt;/a&gt; with worked examples.&lt;/li&gt;
&lt;li&gt;Pick a &lt;a href="https://github.com/paladini/awesome-wonderlic/tree/main/docs/study-plans" rel="noopener noreferrer"&gt;study plan&lt;/a&gt; and follow it.&lt;/li&gt;
&lt;li&gt;Drill the &lt;a href="https://github.com/paladini/awesome-wonderlic/tree/main/data/questions" rel="noopener noreferrer"&gt;question bank&lt;/a&gt; and take the &lt;a href="https://github.com/paladini/awesome-wonderlic/tree/main/practice-tests" rel="noopener noreferrer"&gt;full timed practice tests&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is enough to go from "I have no idea what this test looks like" to "I have timed reps and a strategy" in an evening.&lt;/p&gt;

&lt;h2&gt;
  
  
  For builders: it is also a structured dataset
&lt;/h2&gt;

&lt;p&gt;If you are the kind of person who learns by building, the repo gives you more than markdown pages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JSON Schema validation in CI (&lt;code&gt;data/schema/question.schema.json&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Export scripts for CSV / Markdown / Anki&lt;/li&gt;
&lt;li&gt;A practice-test generator with seed support for reproducible runs&lt;/li&gt;
&lt;li&gt;Python 3 tooling with a single dependency (&lt;code&gt;jsonschema&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You could fork it, build your own drill app, wire it into spaced repetition, or contribute new original questions that pass validation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why open source for this?
&lt;/h2&gt;

&lt;p&gt;Hiring screens are already stressful. Prep material should not add a second paywall on top of the job search.&lt;/p&gt;

&lt;p&gt;An open repo also means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Transparency&lt;/strong&gt; — every question has an explanation you can inspect&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Community improvement&lt;/strong&gt; — better wording, new items, translations of techniques&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No lock-in&lt;/strong&gt; — export to Anki, print a practice test, or generate your own&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Contributions are welcome: original questions, better explanations, vetted links, tooling improvements. Read &lt;a href="https://github.com/paladini/awesome-wonderlic/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;CONTRIBUTING.md&lt;/a&gt; first — it documents the curation rules and the question authoring guide.&lt;/p&gt;

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

&lt;p&gt;Repo: &lt;strong&gt;&lt;a href="https://github.com/paladini/awesome-wonderlic" rel="noopener noreferrer"&gt;github.com/paladini/awesome-wonderlic&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you are preparing for a Wonderlic or Select screen, start with a timed practice test this week. Log your misses by category. Repeat under real conditions. That beats reading about the test for the tenth time.&lt;/p&gt;

&lt;p&gt;If you are hiring and use these assessments, I hope this gives candidates a fairer shot at showing up prepared — without paying for ads first.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This project is not affiliated with, sponsored by, or endorsed by Wonderlic, Inc. "Wonderlic" is a trademark of Wonderlic, Inc., used here nominatively to describe the subject of this educational resource. Every question in the repository is an original work that mirrors the publicly documented format, categories, and difficulty curve of the test — not proprietary Wonderlic items.&lt;/p&gt;




&lt;p&gt;Have you run into Wonderlic or Select in a hiring process recently? What caught you off guard — the pacing, the personality section, or something else? I would love to hear what would make the repo more useful.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
