<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Marc Nuri - Blogging about business and technology</title>
        <link>https://blog.marcnuri.com</link>
        <description>Blogging about business and technology</description>
        <lastBuildDate>Fri, 06 Mar 2026 13:42:18 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>www.marcnuri.com</generator>
        <language>en-US</language>
        <atom:link href="https://blog.marcnuri.com/feed.atom.xml" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Fabric8 Kubernetes Client 7.6 is now available!]]></title>
            <link>https://blog.marcnuri.com/fabric8-kubernetes-client-7-6</link>
            <guid isPermaLink="false">https://blog.marcnuri.com/fabric8-kubernetes-client-7-6</guid>
            <pubDate>Tue, 03 Mar 2026 14:00:00 GMT</pubDate>
            <description><![CDATA[Fabric8 Kubernetes Client 7.6 is available! Check out the major changes and learn how you can contribute.]]></description>
            <content:encoded><![CDATA[
    <div><a href="https://blog.marcnuri.com/fabric8-kubernetes-client-7-6">Original post</a></div>
    <p>On behalf of the <a class="post-link " title="Kubernetes Client for Java: Fabric8 introduction" href="/kubernetes-client-java-fabric8-introduction">Fabric8</a>
team and everyone who has contributed, I'm happy to announce that the Fabric8 Kubernetes Client <code>7.6.1</code> has been
<a href="https://github.com/fabric8io/kubernetes-client/releases/tag/v7.6.1" rel="noopener" title="Link to https://github.com/fabric8io/kubernetes-client/releases/tag/v7.6.1" aria-label="released" target="_blank">released</a> and is now available from
<a href="https://repo1.maven.org/maven2/io/fabric8/kubernetes-client/7.6.1/" rel="noopener" title="Link to https://repo1.maven.org/maven2/io/fabric8/kubernetes-client/7.6.1/" aria-label="Maven Central" target="_blank">Maven Central</a> 🎉.</p>
<p>This marks the sixth minor release of the Fabric8 Kubernetes Client 7, bringing new features, bug fixes, and improvements while keeping the breaking changes minimal.</p>
<p>Thanks to all of you who have contributed with issue reports, pull requests, feedback, and spreading the word with blogs, videos, comments, and so on.
We really appreciate your help, keep it up!</p>
<h2 class="heading"><a href="https://blog.marcnuri.com/fabric8-kubernetes-client-7-6#whats-new" aria-label="whats-new permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="whats-new"></span>What's new?</h2>
<p>Without further ado, let's have a look at the most significant updates:</p>
<ul>
<li><a class="post-link " title="Link to the Kubernetes 1.35 section" href="/fabric8-kubernetes-client-7-6#kubernetes-135">Kubernetes 1.35 support (Timbernetes)</a></li>
<li><a class="post-link " title="Link to the Vert.x 5 section" href="/fabric8-kubernetes-client-7-6#vertx-5">New Vert.x 5 HTTP client implementation</a></li>
<li><a class="post-link " title="Link to the OkHttp 5 section" href="/fabric8-kubernetes-client-7-6#okhttp-5">OkHttp upgraded to version 5</a></li>
<li>🐛 Many other bug fixes and minor improvements</li>
</ul>
<p>You can find the full changelog for this version in our GitHub <a href="https://github.com/fabric8io/kubernetes-client/releases/tag/v7.6.1" rel="noopener" title="Link to https://github.com/fabric8io/kubernetes-client/releases/tag/v7.6.1" aria-label="release page" target="_blank">release page</a>.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/fabric8-kubernetes-client-7-6#kubernetes-135" aria-label="kubernetes-135 permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="kubernetes-135"></span>Kubernetes 1.35 support (Timbernetes)</h3>
<p>This release adds support for Kubernetes v1.35 (Timbernetes), ensuring you have access to the latest API resources and CRDs.</p>
<div class="admonition admonition__note"><p class="admonition-title"><i class="admonition-title-icon fa-solid fa-circle-info"></i>Note</p><div class="admonition-content">
<p>Please note that you can still access newer Kubernetes clusters with <strong>older</strong> versions of the Fabric8 client.</p>
<p>The client provides a GenericKubernetesResources class to interact with resources that are not yet supported by the client.
We do recommend to always use the <strong>latest</strong> version of the client to benefit from the latest features and bug fixes, but it's not mandatory.</p>
</div></div>
<h3 class="heading"><a href="https://blog.marcnuri.com/fabric8-kubernetes-client-7-6#vertx-5" aria-label="vertx-5 permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="vertx-5"></span>New Vert.x 5 HTTP client implementation</h3>
<p>A new Vert.x 5 HTTP client implementation has been added with improved async handling and WebSocket separation.
The <code>kubernetes-httpclient-vertx-5</code> module includes a runtime check for Vert.x 5 classes to provide a clear error message when a Vert.x 4/5 conflict occurs.</p>
<p>The HTTP client factory priority has also been fixed: <code>VertxHttpClientFactory</code> (the default) now has priority -1, while <code>OkHttpClientFactory</code> is restored to priority 0.</p>
<p>Note that the <code>kubernetes-httpclient-vertx</code> (Vert.x 4.x) and <code>kubernetes-httpclient-vertx-5</code> (Vert.x 5.x) modules are <strong>mutually exclusive</strong> and must not be included together in your project dependencies.
When using Vert.x 5, exclude the default Vert.x 4 client and add the Vert.x 5 module:</p>
<div class="code" style="background:#232323"><pre style="display:block;overflow-x:auto;padding:0.5em;background:#232323;color:#e6e1dc"><code class="language-xml" style="white-space:pre"><span style="color:#e8bf6a">&lt;</span><span style="color:#e8bf6a">dependency</span><span style="color:#e8bf6a">&gt;</span><span>
</span><span>  </span><span style="color:#e8bf6a">&lt;</span><span style="color:#e8bf6a">groupId</span><span style="color:#e8bf6a">&gt;</span><span>io.fabric8</span><span style="color:#e8bf6a">&lt;/</span><span style="color:#e8bf6a">groupId</span><span style="color:#e8bf6a">&gt;</span><span>
</span><span>  </span><span style="color:#e8bf6a">&lt;</span><span style="color:#e8bf6a">artifactId</span><span style="color:#e8bf6a">&gt;</span><span>kubernetes-client</span><span style="color:#e8bf6a">&lt;/</span><span style="color:#e8bf6a">artifactId</span><span style="color:#e8bf6a">&gt;</span><span>
</span><span>  </span><span style="color:#e8bf6a">&lt;</span><span style="color:#e8bf6a">exclusions</span><span style="color:#e8bf6a">&gt;</span><span>
</span><span>    </span><span style="color:#e8bf6a">&lt;</span><span style="color:#e8bf6a">exclusion</span><span style="color:#e8bf6a">&gt;</span><span>
</span><span>      </span><span style="color:#e8bf6a">&lt;</span><span style="color:#e8bf6a">groupId</span><span style="color:#e8bf6a">&gt;</span><span>io.fabric8</span><span style="color:#e8bf6a">&lt;/</span><span style="color:#e8bf6a">groupId</span><span style="color:#e8bf6a">&gt;</span><span>
</span><span>      </span><span style="color:#e8bf6a">&lt;</span><span style="color:#e8bf6a">artifactId</span><span style="color:#e8bf6a">&gt;</span><span>kubernetes-httpclient-vertx</span><span style="color:#e8bf6a">&lt;/</span><span style="color:#e8bf6a">artifactId</span><span style="color:#e8bf6a">&gt;</span><span>
</span><span>    </span><span style="color:#e8bf6a">&lt;/</span><span style="color:#e8bf6a">exclusion</span><span style="color:#e8bf6a">&gt;</span><span>
</span><span>  </span><span style="color:#e8bf6a">&lt;/</span><span style="color:#e8bf6a">exclusions</span><span style="color:#e8bf6a">&gt;</span><span>
</span><span></span><span style="color:#e8bf6a">&lt;/</span><span style="color:#e8bf6a">dependency</span><span style="color:#e8bf6a">&gt;</span><span>
</span><span></span><span style="color:#e8bf6a">&lt;</span><span style="color:#e8bf6a">dependency</span><span style="color:#e8bf6a">&gt;</span><span>
</span><span>  </span><span style="color:#e8bf6a">&lt;</span><span style="color:#e8bf6a">groupId</span><span style="color:#e8bf6a">&gt;</span><span>io.fabric8</span><span style="color:#e8bf6a">&lt;/</span><span style="color:#e8bf6a">groupId</span><span style="color:#e8bf6a">&gt;</span><span>
</span><span>  </span><span style="color:#e8bf6a">&lt;</span><span style="color:#e8bf6a">artifactId</span><span style="color:#e8bf6a">&gt;</span><span>kubernetes-httpclient-vertx-5</span><span style="color:#e8bf6a">&lt;/</span><span style="color:#e8bf6a">artifactId</span><span style="color:#e8bf6a">&gt;</span><span>
</span><span></span><span style="color:#e8bf6a">&lt;/</span><span style="color:#e8bf6a">dependency</span><span style="color:#e8bf6a">&gt;</span></code></pre></div>
<h3 class="heading"><a href="https://blog.marcnuri.com/fabric8-kubernetes-client-7-6#okhttp-5" aria-label="okhttp-5 permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="okhttp-5"></span>OkHttp upgraded to version 5</h3>
<p>OkHttp has been upgraded from version 4.12.0 to 5.3.2.
This update removes internal API usage and fixes deprecated OkHttp 5 calls.
While the versions are binary compatible, the major version upgrade might cause side effects in some projects.</p>
<h2 class="heading"><a href="https://blog.marcnuri.com/fabric8-kubernetes-client-7-6#using-this-release" aria-label="using-this-release permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="using-this-release"></span>Using this release</h2>
<p>If your project is based on Maven, you just need to add the Fabric8 Kubernetes Client to your Maven dependencies:</p>
<div class="code" style="background:#232323"><pre style="display:block;overflow-x:auto;padding:0.5em;background:#232323;color:#e6e1dc"><code class="language-xml" style="white-space:pre"><span style="color:#e8bf6a">&lt;</span><span style="color:#e8bf6a">dependency</span><span style="color:#e8bf6a">&gt;</span><span>
</span><span>  </span><span style="color:#e8bf6a">&lt;</span><span style="color:#e8bf6a">groupId</span><span style="color:#e8bf6a">&gt;</span><span>io.fabric8</span><span style="color:#e8bf6a">&lt;/</span><span style="color:#e8bf6a">groupId</span><span style="color:#e8bf6a">&gt;</span><span>
</span><span>  </span><span style="color:#e8bf6a">&lt;</span><span style="color:#e8bf6a">artifactId</span><span style="color:#e8bf6a">&gt;</span><span>kubernetes-client</span><span style="color:#e8bf6a">&lt;/</span><span style="color:#e8bf6a">artifactId</span><span style="color:#e8bf6a">&gt;</span><span>
</span><span>  </span><span style="color:#e8bf6a">&lt;</span><span style="color:#e8bf6a">version</span><span style="color:#e8bf6a">&gt;</span><span>7.6.1</span><span style="color:#e8bf6a">&lt;/</span><span style="color:#e8bf6a">version</span><span style="color:#e8bf6a">&gt;</span><span>
</span><span></span><span style="color:#e8bf6a">&lt;/</span><span style="color:#e8bf6a">dependency</span><span style="color:#e8bf6a">&gt;</span></code></pre></div>
<p>If your project is based on Gradle, you just need to add the Fabric8 Kubernetes Client to your Gradle dependencies:</p>
<div class="code" style="background:#232323"><pre style="display:block;overflow-x:auto;padding:0.5em;background:#232323;color:#e6e1dc"><code class="language-groovy" style="white-space:pre"><span>dependencies {
</span><span>  api </span><span style="color:#a5c261">"io.fabric8:kubernetes-client:7.6.1"</span><span>
</span>}</code></pre></div>
<p>Once your project is ready, you can create a new instance of the client to perform operations.
In the following code snippet, I show you how to instantiate the client and retrieve a list of Pods:</p>
<div class="code" style="background:#232323"><pre style="display:block;overflow-x:auto;padding:0.5em;background:#232323;color:#e6e1dc"><code class="language-java" style="white-space:pre"><span style="color:#c26230">try</span><span> (KubernetesClient client = </span><span style="color:#c26230">new</span><span> KubernetesClientBuilder().build()) {
</span>  client.pods().list().getItems().forEach(p -&gt; System.out.println(p.getMetadata().getName()));
<!-- -->}</code></pre></div>
<h2 class="heading"><a href="https://blog.marcnuri.com/fabric8-kubernetes-client-7-6#how-can-you-help" aria-label="how-can-you-help permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="how-can-you-help"></span>How can you help?</h2>
<p>If you're interested in helping out and are a first-time contributor, check out
the <a href="https://github.com/fabric8io/kubernetes-client/labels/good%20first%20issue" rel="noopener" title="Link to https://github.com/fabric8io/kubernetes-client/labels/good%20first%20issue" aria-label="&quot;good first issue&quot;" target="_blank">"good first issue"</a> tag in the issue repository.
We've tagged extremely easy issues so that you can get started contributing to Open Source.</p>
<p>We're also excited to read articles and posts mentioning our project and sharing the user experience.
Giving a star to the project, and spreading the word in general, helps us reach more users and broaden the
feedback. Feedback is the only way to improve.</p>
<p><a href="https://github.com/fabric8io/kubernetes-client" rel="noopener" title="Link to https://github.com/fabric8io/kubernetes-client" aria-label="Project Page" target="_blank">Project Page</a> |
<a href="https://github.com/fabric8io/kubernetes-client/issues" rel="noopener" title="Link to https://github.com/fabric8io/kubernetes-client/issues" aria-label="Issues" target="_blank">Issues</a> |
<a href="https://github.com/fabric8io/kubernetes-client/discussions" rel="noopener" title="Link to https://github.com/fabric8io/kubernetes-client/discussions" aria-label="Discussions" target="_blank">Discussions</a> |
<a href="https://gitter.im/fabric8io/kubernetes-client" rel="noopener" title="Link to https://gitter.im/fabric8io/kubernetes-client" aria-label="Gitter" target="_blank">Gitter</a> |
<a href="https://stackoverflow.com/questions/tagged/fabric8" rel="noopener" title="Link to https://stackoverflow.com/questions/tagged/fabric8" aria-label="Stack Overflow" target="_blank">Stack Overflow</a></p>
<span class="post-image__pusher "></span><figure class="post-image "><span class="post-image__scrim"></span><a href="/static/eb11f2059915fc28b71c76eff27dc487/539a4/fabric8-logo.png" class="post-image__link" title="The logo of Fabric8 Kubernetes Client"><span class="post-image__image-container"><div data-gatsby-image-wrapper="" class="gatsby-image-wrapper gatsby-image-wrapper-constrained post-image__image "><picture><source type="image/webp" data-srcset="/static/eb11f2059915fc28b71c76eff27dc487/a66c2/fabric8-logo.webp 269w,/static/eb11f2059915fc28b71c76eff27dc487/3860d/fabric8-logo.webp 539w,/static/eb11f2059915fc28b71c76eff27dc487/418ed/fabric8-logo.webp 1077w" sizes="(min-width: 1077px) 1077px, 100vw"><img data-gatsby-image-ssr="" data-main-image="" style="opacity: 1;" sizes="(min-width: 1077px) 1077px, 100vw" decoding="async" loading="lazy" data-src="/static/eb11f2059915fc28b71c76eff27dc487/539a4/fabric8-logo.png" data-srcset="/static/eb11f2059915fc28b71c76eff27dc487/53e69/fabric8-logo.png 269w,/static/eb11f2059915fc28b71c76eff27dc487/f0c2e/fabric8-logo.png 539w,/static/eb11f2059915fc28b71c76eff27dc487/539a4/fabric8-logo.png 1077w" alt="The logo of Fabric8 Kubernetes Client" src="https://blog.marcnuri.com/static/eb11f2059915fc28b71c76eff27dc487/539a4/fabric8-logo.png" srcset="https://blog.marcnuri.com/static/eb11f2059915fc28b71c76eff27dc487/53e69/fabric8-logo.png 269w,https://blog.marcnuri.com/static/eb11f2059915fc28b71c76eff27dc487/f0c2e/fabric8-logo.png 539w,https://blog.marcnuri.com/static/eb11f2059915fc28b71c76eff27dc487/539a4/fabric8-logo.png 1077w"></picture><script type="module">const t="undefined"!=typeof HTMLImageElement&&"loading"in HTMLImageElement.prototype;if(t){const t=document.querySelectorAll("img[data-main-image]");for(let e of t){e.dataset.src&&(e.setAttribute("src",e.dataset.src),e.removeAttribute("data-src")),e.dataset.srcset&&(e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset"));const t=e.parentNode.querySelectorAll("source[data-srcset]");for(let e of t)e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset");e.complete&&(e.style.opacity=1,e.parentNode.parentNode.querySelector("[data-placeholder-image]").style.opacity=0)}}</script></div></span></a></figure>
  ]]></content:encoded>
            <category>Cloud Native</category>
            <enclosure url="https://blog.marcnuri.com/static/b9dbe50d753954e05cf28f5e3aa1c88a/baaed/fabric8.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[AI Coding Agent Dashboard: Orchestrating Claude Code Across Devices]]></title>
            <link>https://blog.marcnuri.com/ai-coding-agent-dashboard</link>
            <guid isPermaLink="false">https://blog.marcnuri.com/ai-coding-agent-dashboard</guid>
            <pubDate>Mon, 23 Feb 2026 18:00:00 GMT</pubDate>
            <description><![CDATA[Learn how I built a real-time dashboard to monitor and orchestrate multiple AI coding agents running in parallel across projects and devices.]]></description>
            <content:encoded><![CDATA[
    <div><a href="https://blog.marcnuri.com/ai-coding-agent-dashboard">Original post</a></div>
    <h2 class="heading"><a href="https://blog.marcnuri.com/ai-coding-agent-dashboard#introduction" aria-label="introduction permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="introduction"></span>Introduction</h2>
<p>In December 2025 I wrote about <a class="post-link " title="Boosting My Developer Productivity with AI in 2025" href="/boosting-developer-productivity-ai-2025">becoming a coding orchestrator</a>, shifting my role from implementer to someone who directs multiple <a class="tag-link " title="AI Agent" aria-label="AI coding agents" href="/tag/ai-agent">AI coding agents</a> working in parallel.
The productivity gains were real, but so was the chaos.</p>
<p>Running 5 to 10 Claude Code sessions across multiple machines, different projects, and various git branches quickly became unmanageable.
I'd forget about sessions, lose track of what was running where, and sometimes only rediscover abandoned work hours or days later.</p>
<p>Terminal tabs and tmux solve local organization.
They do not solve cross-device visibility.
I needed a single view of everything, so I built a dashboard.</p>
<h2 class="heading"><a href="https://blog.marcnuri.com/ai-coding-agent-dashboard#the-problem-orchestrating-without-visibility" aria-label="the-problem-orchestrating-without-visibility permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="the-problem-orchestrating-without-visibility"></span>The Problem: Orchestrating Without Visibility</h2>
<p>My daily setup involves a MacBook for on-the-go work and a Linux workstation for heavier tasks.
On any given day, I might have Claude Code sessions running across both machines, each tackling different tasks in different projects and branches.</p>
<p>The cognitive overhead of tracking which agent is doing what, on which machine, adds up fast.
You have to remember which terminal window maps to which project, which branch each agent is working on, whether a session is still active or has stalled, and whether the agent is waiting for permission or has already finished.</p>
<p>Without a centralized view, the orchestrator workflow I described in my <a class="post-link " title="Boosting My Developer Productivity with AI in 2025" href="/boosting-developer-productivity-ai-2025">productivity post</a> breaks down.
You lose the parallelism advantage that makes AI-assisted development so powerful.</p>
<p>Existing tools only solve parts of this problem.
Tmux and terminal multiplexers work great on a single machine, but they don't span devices.
IDE-based solutions are tied to a specific editor.
What I needed was a tool that could aggregate information from all my machines and present it in a single, real-time view.</p>
<p>As I discussed in <a class="post-link " title="The Future of Developer Tools: Adapting to Machine-Based Developers" href="/the-future-of-developer-tools-machine-based-developers">The Future of Developer Tools</a>, our tools need to adapt to AI-agent workflows.
This dashboard is one example of what that adaptation looks like in practice.</p>
<h2 class="heading"><a href="https://blog.marcnuri.com/ai-coding-agent-dashboard#context-switching-and-cognitive-load" aria-label="context-switching-and-cognitive-load permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="context-switching-and-cognitive-load"></span>The Real Challenge: Context Switching and Cognitive Load</h2>
<p>One thing that doesn't get discussed enough is that the biggest bottleneck in AI-assisted parallel development isn't the AI itself.
<strong>It's the human</strong>.</p>
<p>When you're orchestrating multiple agents, the cognitive load of context switching between sessions becomes the primary constraint.
Which agent was working on what?
Did that session finish or stall?
Is the agent waiting for my input?
Every time you have to answer these questions by opening terminals and inspecting state manually, you pay a context-switching tax that eats into the productivity gains you're trying to achieve.</p>
<p>This is the core challenge of the orchestrator era: <strong>managing the mental overhead of parallel work</strong>.
Any tool that claims to support AI-assisted development needs to address this head-on.</p>
<h2 class="heading"><a href="https://blog.marcnuri.com/ai-coding-agent-dashboard#the-dashboard-what-it-does" aria-label="the-dashboard-what-it-does permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="the-dashboard-what-it-does"></span>The Dashboard: What It Does</h2>
<span class="post-image__pusher "></span><figure class="post-image "><span class="post-image__scrim"></span><a href="/static/bc3b8c8feb9da0aacb9adc01963f55a9/7192e/dashboard-overview.png" class="post-image__link" title="AI Coding Agent Dashboard overview showing sessions organized by device"><span class="post-image__image-container"><div data-gatsby-image-wrapper="" class="gatsby-image-wrapper gatsby-image-wrapper-constrained post-image__image "><picture><source type="image/webp" data-srcset="/static/bc3b8c8feb9da0aacb9adc01963f55a9/4b64d/dashboard-overview.webp 458w,/static/bc3b8c8feb9da0aacb9adc01963f55a9/3ca0d/dashboard-overview.webp 916w,/static/bc3b8c8feb9da0aacb9adc01963f55a9/01aaf/dashboard-overview.webp 1832w" sizes="(min-width: 1832px) 1832px, 100vw"><img data-gatsby-image-ssr="" data-main-image="" style="opacity: 1;" sizes="(min-width: 1832px) 1832px, 100vw" decoding="async" loading="lazy" data-src="/static/bc3b8c8feb9da0aacb9adc01963f55a9/7192e/dashboard-overview.png" data-srcset="/static/bc3b8c8feb9da0aacb9adc01963f55a9/00c70/dashboard-overview.png 458w,/static/bc3b8c8feb9da0aacb9adc01963f55a9/9bf80/dashboard-overview.png 916w,/static/bc3b8c8feb9da0aacb9adc01963f55a9/7192e/dashboard-overview.png 1832w" alt="AI Coding Agent Dashboard overview showing sessions organized by device" src="https://blog.marcnuri.com/static/bc3b8c8feb9da0aacb9adc01963f55a9/7192e/dashboard-overview.png" srcset="https://blog.marcnuri.com/static/bc3b8c8feb9da0aacb9adc01963f55a9/00c70/dashboard-overview.png 458w,https://blog.marcnuri.com/static/bc3b8c8feb9da0aacb9adc01963f55a9/9bf80/dashboard-overview.png 916w,https://blog.marcnuri.com/static/bc3b8c8feb9da0aacb9adc01963f55a9/7192e/dashboard-overview.png 1832w"></picture><script type="module">const t="undefined"!=typeof HTMLImageElement&&"loading"in HTMLImageElement.prototype;if(t){const t=document.querySelectorAll("img[data-main-image]");for(let e of t){e.dataset.src&&(e.setAttribute("src",e.dataset.src),e.removeAttribute("data-src")),e.dataset.srcset&&(e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset"));const t=e.parentNode.querySelectorAll("source[data-srcset]");for(let e of t)e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset");e.complete&&(e.style.opacity=1,e.parentNode.parentNode.querySelector("[data-placeholder-image]").style.opacity=0)}}</script></div></span></a></figure>
<p>The dashboard provides a real-time overview of all my active coding agent sessions across every device and project.</p>
<p>Sessions are organized by device, so I can immediately see what's running on my MacBook versus my workstation.
Each session is represented by a card that displays key information at a glance:</p>
<ul>
<li><strong>Project and git branch</strong>: Which repository and branch the agent is working on.</li>
<li><strong>Pull Request link</strong>: If the agent has created or is working on a PR, a direct link is shown.</li>
<li><strong>Model and context usage</strong>: Which <a class="tag-link " title="LLM" aria-label="LLM" href="/tag/llm">LLM</a> model the session is using and how much of the context window has been consumed.</li>
<li><strong>Status</strong>: Whether the agent is actively working, idle, or waiting for user permission.</li>
<li><strong>Task description</strong>: A summary of what the agent is currently doing.</li>
<li><strong>MCP servers</strong>: Which <a class="post-link " title="Introduction to the Model Context Protocol (MCP): The Future of AI Integration" href="/model-context-protocol-mcp-introduction">Model Context Protocol</a> servers are connected to the session.</li>
</ul>
<div style="max-width:400px;margin:0 auto"><span class="post-image__pusher "></span><figure class="post-image "><span class="post-image__scrim"></span><a href="/static/2e517de3ea8ebf0070ac54343a6d75c7/eca67/session-card-detail.png" class="post-image__link" title="Close-up of a session card showing project, branch, model, context usage, and status"><span class="post-image__image-container"><div data-gatsby-image-wrapper="" class="gatsby-image-wrapper gatsby-image-wrapper-constrained post-image__image "><picture><source type="image/webp" data-srcset="/static/2e517de3ea8ebf0070ac54343a6d75c7/f2825/session-card-detail.webp 199w,/static/2e517de3ea8ebf0070ac54343a6d75c7/0c980/session-card-detail.webp 397w,/static/2e517de3ea8ebf0070ac54343a6d75c7/43896/session-card-detail.webp 794w" sizes="(min-width: 794px) 794px, 100vw"><img data-gatsby-image-ssr="" data-main-image="" style="opacity: 1;" sizes="(min-width: 794px) 794px, 100vw" decoding="async" loading="lazy" data-src="/static/2e517de3ea8ebf0070ac54343a6d75c7/eca67/session-card-detail.png" data-srcset="/static/2e517de3ea8ebf0070ac54343a6d75c7/88b5f/session-card-detail.png 199w,/static/2e517de3ea8ebf0070ac54343a6d75c7/5d600/session-card-detail.png 397w,/static/2e517de3ea8ebf0070ac54343a6d75c7/eca67/session-card-detail.png 794w" alt="Close-up of a session card showing project, branch, model, context usage, and status" src="https://blog.marcnuri.com/static/2e517de3ea8ebf0070ac54343a6d75c7/eca67/session-card-detail.png" srcset="https://blog.marcnuri.com/static/2e517de3ea8ebf0070ac54343a6d75c7/88b5f/session-card-detail.png 199w,https://blog.marcnuri.com/static/2e517de3ea8ebf0070ac54343a6d75c7/5d600/session-card-detail.png 397w,https://blog.marcnuri.com/static/2e517de3ea8ebf0070ac54343a6d75c7/eca67/session-card-detail.png 794w"></picture><script type="module">const t="undefined"!=typeof HTMLImageElement&&"loading"in HTMLImageElement.prototype;if(t){const t=document.querySelectorAll("img[data-main-image]");for(let e of t){e.dataset.src&&(e.setAttribute("src",e.dataset.src),e.removeAttribute("data-src")),e.dataset.srcset&&(e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset"));const t=e.parentNode.querySelectorAll("source[data-srcset]");for(let e of t)e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset");e.complete&&(e.style.opacity=1,e.parentNode.parentNode.querySelector("[data-placeholder-image]").style.opacity=0)}}</script></div></span></a></figure></div>
<p>The dashboard updates in real time as agents report their state.
It uses this stream of information to detect stale or crashed sessions automatically.
If an agent stops reporting for too long, the session card reflects that something may have gone wrong.</p>
<p>By externalizing this state, the dashboard directly attacks the context-switching problem I described above.
Instead of cycling through terminal windows to reconstruct what's happening, I can glance at the dashboard and know the state of every agent instantly.</p>
<h2 class="heading"><a href="https://blog.marcnuri.com/ai-coding-agent-dashboard#remote-terminal-attachment" aria-label="remote-terminal-attachment permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="remote-terminal-attachment"></span>Remote Terminal Attachment</h2>
<p>The feature that changed everything for me is the ability to connect to any agent session directly from the browser.</p>
<p>I've seen developers set up elaborate solutions to achieve cross-device terminal access: Tailscale VPNs, SSH tunneling from their phones, complex tmux configurations with nested sessions, and custom scripts to keep everything in sync.
These approaches work, but they add layers of friction and configuration that become their own maintenance burden.</p>
<span class="post-image__pusher "></span><figure class="post-image "><span class="post-image__scrim"></span><a href="/static/3d50657667ae2381bbb22da456755d6f/302b5/terminal-attachment.png" class="post-image__link" title="Browser-based terminal attached to a remote coding agent session"><span class="post-image__image-container"><div data-gatsby-image-wrapper="" class="gatsby-image-wrapper gatsby-image-wrapper-constrained post-image__image "><picture><source type="image/webp" data-srcset="/static/3d50657667ae2381bbb22da456755d6f/d052c/terminal-attachment.webp 414w,/static/3d50657667ae2381bbb22da456755d6f/28ff6/terminal-attachment.webp 828w,/static/3d50657667ae2381bbb22da456755d6f/9ed7e/terminal-attachment.webp 1656w" sizes="(min-width: 1656px) 1656px, 100vw"><img data-gatsby-image-ssr="" data-main-image="" style="opacity: 1;" sizes="(min-width: 1656px) 1656px, 100vw" decoding="async" loading="lazy" data-src="/static/3d50657667ae2381bbb22da456755d6f/302b5/terminal-attachment.png" data-srcset="/static/3d50657667ae2381bbb22da456755d6f/9451b/terminal-attachment.png 414w,/static/3d50657667ae2381bbb22da456755d6f/22d71/terminal-attachment.png 828w,/static/3d50657667ae2381bbb22da456755d6f/302b5/terminal-attachment.png 1656w" alt="Browser-based terminal attached to a remote coding agent session" src="https://blog.marcnuri.com/static/3d50657667ae2381bbb22da456755d6f/302b5/terminal-attachment.png" srcset="https://blog.marcnuri.com/static/3d50657667ae2381bbb22da456755d6f/9451b/terminal-attachment.png 414w,https://blog.marcnuri.com/static/3d50657667ae2381bbb22da456755d6f/22d71/terminal-attachment.png 828w,https://blog.marcnuri.com/static/3d50657667ae2381bbb22da456755d6f/302b5/terminal-attachment.png 1656w"></picture><script type="module">const t="undefined"!=typeof HTMLImageElement&&"loading"in HTMLImageElement.prototype;if(t){const t=document.querySelectorAll("img[data-main-image]");for(let e of t){e.dataset.src&&(e.setAttribute("src",e.dataset.src),e.removeAttribute("data-src")),e.dataset.srcset&&(e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset"));const t=e.parentNode.querySelectorAll("source[data-srcset]");for(let e of t)e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset");e.complete&&(e.style.opacity=1,e.parentNode.parentNode.querySelector("[data-placeholder-image]").style.opacity=0)}}</script></div></span></a></figure>
<p>The dashboard takes a different approach.
When I click on a session, I get a live terminal embedded right in the browser, attached to that agent's session.
I can read the output, provide input, approve permissions, or intervene if the agent goes off track.
No SSH setup, no VPN, no terminal app on my phone.
Just a browser and the same interface everywhere.</p>
<p>This works from any device: my laptop, my phone, a tablet, or any machine with a web browser.
During lunch, I can check on a long-running session from my phone.
On the couch in the evening, I can review what an agent produced during the day from a tablet.
The UX is the same everywhere because the interface is always the same web application.</p>
<p>Security was a primary design concern from the start.
This runs in my own environment with authenticated device registration and scoped access, so the goal is convenience without exposing raw terminal access publicly.</p>
<div><iframe src="https://www.youtube.com/embed/CY70iLBwbrg" class="aligncenter " width="560" height="315" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe></div>
<h2 class="heading"><a href="https://blog.marcnuri.com/ai-coding-agent-dashboard#spawning-sessions-remotely" aria-label="spawning-sessions-remotely permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="spawning-sessions-remotely"></span>Spawning Sessions Remotely</h2>
<p>If remote terminal attachment is the feature that keeps me connected, remote spawning is the one that keeps me productive from anywhere.</p>
<div style="max-width:600px;margin:0 auto"><span class="post-image__pusher "></span><figure class="post-image "><span class="post-image__scrim"></span><a href="/static/b6327da9b4333177e0fa93e1c8524df5/178c1/spawn-session.png" class="post-image__link" title="New session dialog for spawning a coding agent on a remote device"><span class="post-image__image-container"><div data-gatsby-image-wrapper="" class="gatsby-image-wrapper gatsby-image-wrapper-constrained post-image__image "><picture><source type="image/webp" data-srcset="/static/b6327da9b4333177e0fa93e1c8524df5/86c34/spawn-session.webp 300w,/static/b6327da9b4333177e0fa93e1c8524df5/e82c4/spawn-session.webp 600w,/static/b6327da9b4333177e0fa93e1c8524df5/a842b/spawn-session.webp 1200w" sizes="(min-width: 1200px) 1200px, 100vw"><img data-gatsby-image-ssr="" data-main-image="" style="opacity: 1;" sizes="(min-width: 1200px) 1200px, 100vw" decoding="async" loading="lazy" data-src="/static/b6327da9b4333177e0fa93e1c8524df5/178c1/spawn-session.png" data-srcset="/static/b6327da9b4333177e0fa93e1c8524df5/8fe8f/spawn-session.png 300w,/static/b6327da9b4333177e0fa93e1c8524df5/59f64/spawn-session.png 600w,/static/b6327da9b4333177e0fa93e1c8524df5/178c1/spawn-session.png 1200w" alt="New session dialog for spawning a coding agent on a remote device" src="https://blog.marcnuri.com/static/b6327da9b4333177e0fa93e1c8524df5/178c1/spawn-session.png" srcset="https://blog.marcnuri.com/static/b6327da9b4333177e0fa93e1c8524df5/8fe8f/spawn-session.png 300w,https://blog.marcnuri.com/static/b6327da9b4333177e0fa93e1c8524df5/59f64/spawn-session.png 600w,https://blog.marcnuri.com/static/b6327da9b4333177e0fa93e1c8524df5/178c1/spawn-session.png 1200w"></picture><script type="module">const t="undefined"!=typeof HTMLImageElement&&"loading"in HTMLImageElement.prototype;if(t){const t=document.querySelectorAll("img[data-main-image]");for(let e of t){e.dataset.src&&(e.setAttribute("src",e.dataset.src),e.removeAttribute("data-src")),e.dataset.srcset&&(e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset"));const t=e.parentNode.querySelectorAll("source[data-srcset]");for(let e of t)e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset");e.complete&&(e.style.opacity=1,e.parentNode.parentNode.querySelector("[data-placeholder-image]").style.opacity=0)}}</script></div></span></a></figure></div>
<p>From the dashboard, I can start a new Claude Code session on any of my registered devices.
I pick a device, select a project from its available repositories, and a fresh agent session spins up in a new tmux window on that machine.
The dashboard picks it up immediately and starts tracking it.</p>
<p>This means I can kick off agent sessions while commuting, then sit down later to review the results.
I can distribute work across machines based on what each one is best suited for, sending resource-intensive tasks to the workstation and lighter ones to the laptop.</p>
<p>The ability to start work from anywhere and review it from anywhere else has fundamentally changed when and where I can be productive.</p>
<div><iframe src="https://www.youtube.com/embed/CzenyaZ4k3o" class="aligncenter " width="560" height="315" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe></div>
<h2 class="heading"><a href="https://blog.marcnuri.com/ai-coding-agent-dashboard#architecture-overview" aria-label="architecture-overview permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="architecture-overview"></span>How It Works: Architecture Overview</h2>
<p>Rather than walking through every implementation detail, I want to highlight the high-level architecture and the design decisions that make this system flexible.</p>
<p><span><svg aria-roledescription="flowchart-v2" role="graphics-document document" viewBox="0 0 819.3125 242" style="max-width: 819.312px; background-color: transparent;" class="flowchart" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="100%" id="mermaid-40"><style>#mermaid-40{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#ccc;}#mermaid-40 .error-icon{fill:#a44141;}#mermaid-40 .error-text{fill:#ddd;stroke:#ddd;}#mermaid-40 .edge-thickness-normal{stroke-width:1px;}#mermaid-40 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-40 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-40 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-40 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-40 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-40 .marker{fill:lightgrey;stroke:lightgrey;}#mermaid-40 .marker.cross{stroke:lightgrey;}#mermaid-40 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-40 p{margin:0;}#mermaid-40 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#ccc;}#mermaid-40 .cluster-label text{fill:#F9FFFE;}#mermaid-40 .cluster-label span{color:#F9FFFE;}#mermaid-40 .cluster-label span p{background-color:transparent;}#mermaid-40 .label text,#mermaid-40 span{fill:#ccc;color:#ccc;}#mermaid-40 .node rect,#mermaid-40 .node circle,#mermaid-40 .node ellipse,#mermaid-40 .node polygon,#mermaid-40 .node path{fill:#1f2020;stroke:#ccc;stroke-width:1px;}#mermaid-40 .rough-node .label text,#mermaid-40 .node .label text,#mermaid-40 .image-shape .label,#mermaid-40 .icon-shape .label{text-anchor:middle;}#mermaid-40 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-40 .rough-node .label,#mermaid-40 .node .label,#mermaid-40 .image-shape .label,#mermaid-40 .icon-shape .label{text-align:center;}#mermaid-40 .node.clickable{cursor:pointer;}#mermaid-40 .root .anchor path{fill:lightgrey!important;stroke-width:0;stroke:lightgrey;}#mermaid-40 .arrowheadPath{fill:lightgrey;}#mermaid-40 .edgePath .path{stroke:lightgrey;stroke-width:2.0px;}#mermaid-40 .flowchart-link{stroke:lightgrey;fill:none;}#mermaid-40 .edgeLabel{background-color:hsl(0, 0%, 34.4117647059%);text-align:center;}#mermaid-40 .edgeLabel p{background-color:hsl(0, 0%, 34.4117647059%);}#mermaid-40 .edgeLabel rect{opacity:0.5;background-color:hsl(0, 0%, 34.4117647059%);fill:hsl(0, 0%, 34.4117647059%);}#mermaid-40 .labelBkg{background-color:rgba(87.75, 87.75, 87.75, 0.5);}#mermaid-40 .cluster rect{fill:hsl(180, 1.5873015873%, 28.3529411765%);stroke:rgba(255, 255, 255, 0.25);stroke-width:1px;}#mermaid-40 .cluster text{fill:#F9FFFE;}#mermaid-40 .cluster span{color:#F9FFFE;}#mermaid-40 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(20, 1.5873015873%, 12.3529411765%);border:1px solid rgba(255, 255, 255, 0.25);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-40 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#ccc;}#mermaid-40 rect.text{fill:none;stroke-width:0;}#mermaid-40 .icon-shape,#mermaid-40 .image-shape{background-color:hsl(0, 0%, 34.4117647059%);text-align:center;}#mermaid-40 .icon-shape p,#mermaid-40 .image-shape p{background-color:hsl(0, 0%, 34.4117647059%);padding:2px;}#mermaid-40 .icon-shape rect,#mermaid-40 .image-shape rect{opacity:0.5;background-color:hsl(0, 0%, 34.4117647059%);fill:hsl(0, 0%, 34.4117647059%);}#mermaid-40 .edgeLabel p{padding:3px 6px;}#mermaid-40 .flowchartTitleText{color:#333;fill:#333;font-size:1.5rem;}#mermaid-40 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}
  /* blog.marcnuri.com - Custom mermaid overrides */
  /* Make arrows darker for better visibility on light backgrounds */
  .edgePath path, .flowchart-link { stroke: #666 !important; stroke-width: 1.5px !important; }
  .marker path { fill: #666 !important; stroke: #666 !important; }
  .arrowMarkerPath { fill: #666 !important; }
</style><g><marker orient="auto" markerHeight="8" markerWidth="8" markerUnits="userSpaceOnUse" refY="5" refX="5" viewBox="0 0 10 10" class="marker flowchart-v2" id="mermaid-40_flowchart-v2-pointEnd"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowMarkerPath" d="M 0 0 L 10 5 L 0 10 z"></path></marker><marker orient="auto" markerHeight="8" markerWidth="8" markerUnits="userSpaceOnUse" refY="5" refX="4.5" viewBox="0 0 10 10" class="marker flowchart-v2" id="mermaid-40_flowchart-v2-pointStart"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowMarkerPath" d="M 0 5 L 10 10 L 10 0 z"></path></marker><marker orient="auto" markerHeight="11" markerWidth="11" markerUnits="userSpaceOnUse" refY="5" refX="11" viewBox="0 0 10 10" class="marker flowchart-v2" id="mermaid-40_flowchart-v2-circleEnd"><circle style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowMarkerPath" r="5" cy="5" cx="5"></circle></marker><marker orient="auto" markerHeight="11" markerWidth="11" markerUnits="userSpaceOnUse" refY="5" refX="-1" viewBox="0 0 10 10" class="marker flowchart-v2" id="mermaid-40_flowchart-v2-circleStart"><circle style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowMarkerPath" r="5" cy="5" cx="5"></circle></marker><marker orient="auto" markerHeight="11" markerWidth="11" markerUnits="userSpaceOnUse" refY="5.2" refX="12" viewBox="0 0 11 11" class="marker cross flowchart-v2" id="mermaid-40_flowchart-v2-crossEnd"><path style="stroke-width: 2; stroke-dasharray: 1, 0;" class="arrowMarkerPath" d="M 1,1 l 9,9 M 10,1 l -9,9"></path></marker><marker orient="auto" markerHeight="11" markerWidth="11" markerUnits="userSpaceOnUse" refY="5.2" refX="-1" viewBox="0 0 11 11" class="marker cross flowchart-v2" id="mermaid-40_flowchart-v2-crossStart"><path style="stroke-width: 2; stroke-dasharray: 1, 0;" class="arrowMarkerPath" d="M 1,1 l 9,9 M 10,1 l -9,9"></path></marker><g class="root"><g class="clusters"></g><g class="edgePaths"><path marker-end="url(#mermaid-40_flowchart-v2-pointEnd)" style="" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" id="L_Phone_Dashboard_0" d="M158.719,64L168.628,64C178.536,64,198.354,64,218.664,67.931C238.973,71.862,259.775,79.724,270.175,83.655L280.576,87.586"></path><path marker-end="url(#mermaid-40_flowchart-v2-pointEnd)" style="" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" id="L_Tablet_Dashboard_1" d="M156.789,178L167.02,178C177.25,178,197.711,178,218.342,174.069C238.973,170.138,259.775,162.276,270.175,158.345L280.576,154.414"></path><path marker-end="url(#mermaid-40_flowchart-v2-pointEnd)" marker-start="url(#mermaid-40_flowchart-v2-pointStart)" style="" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" id="L_Dashboard_Device1_2" d="M449.631,87.464L463.845,81.553C478.058,75.643,506.486,63.821,532.462,57.911C558.438,52,581.961,52,593.723,52L605.484,52"></path><path marker-end="url(#mermaid-40_flowchart-v2-pointEnd)" marker-start="url(#mermaid-40_flowchart-v2-pointStart)" style="" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" id="L_Dashboard_Device2_3" d="M449.631,154.536L463.845,160.447C478.058,166.357,506.486,178.179,532.462,184.089C558.438,190,581.961,190,593.723,190L605.484,190"></path></g><g class="edgeLabels"><g transform="translate(218.171875, 64)" class="edgeLabel"><g transform="translate(-34.453125, -15)" class="label"><foreignObject height="30" width="68.90625"><div style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;" class="labelBkg" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"><p>browser</p></span></div></foreignObject></g></g><g transform="translate(218.171875, 178)" class="edgeLabel"><g transform="translate(-34.453125, -15)" class="label"><foreignObject height="30" width="68.90625"><div style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;" class="labelBkg" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"><p>browser</p></span></div></foreignObject></g></g><g transform="translate(534.9140625, 52)" class="edgeLabel"><g transform="translate(-49.5703125, -27)" class="label"><foreignObject height="54" width="99.140625"><div style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;" class="labelBkg" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"><p>heartbeat <br> terminal I/O&nbsp;</p></span></div></foreignObject></g></g><g transform="translate(534.9140625, 190)" class="edgeLabel"><g transform="translate(-49.5703125, -27)" class="label"><foreignObject height="54" width="99.140625"><div style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;" class="labelBkg" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"><p>heartbeat <br> terminal I/O&nbsp;</p></span></div></foreignObject></g></g></g><g class="nodes"><g transform="translate(83.359375, 64)" id="flowchart-Phone-0" class="node default"><rect height="64" width="150.71875" y="-32" x="-75.359375" style="" class="basic label-container"></rect><g transform="translate(-35.359375, -12)" style="" class="label"><rect></rect><foreignObject height="24" width="70.71875"><div style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"><p>📱 Phone</p></span></div></foreignObject></g></g><g transform="translate(83.359375, 178)" id="flowchart-Tablet-1" class="node default"><rect height="64" width="146.859375" y="-32" x="-73.4296875" style="" class="basic label-container"></rect><g transform="translate(-33.4296875, -12)" style="" class="label"><rect></rect><foreignObject height="24" width="66.859375"><div style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"><p>💻 Tablet</p></span></div></foreignObject></g></g><g transform="translate(368.984375, 121)" id="flowchart-Dashboard-3" class="node default"><rect height="64" width="182.71875" y="-32" x="-91.359375" style="" class="basic label-container"></rect><g transform="translate(-51.359375, -12)" style="" class="label"><rect></rect><foreignObject height="24" width="102.71875"><div style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"><p>📊 Dashboard</p></span></div></foreignObject></g></g><g transform="translate(710.3984375, 52)" id="flowchart-Device1-7" class="node default"><rect height="88" width="201.828125" y="-44" x="-100.9140625" style="" class="basic label-container"></rect><g transform="translate(-60.9140625, -24)" style="" class="label"><rect></rect><foreignObject height="48" width="121.828125"><div style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"><p>🖥️ Device 1<br>Agents · Projects</p></span></div></foreignObject></g></g><g transform="translate(710.3984375, 190)" id="flowchart-Device2-9" class="node default"><rect height="88" width="201.828125" y="-44" x="-100.9140625" style="" class="basic label-container"></rect><g transform="translate(-60.9140625, -24)" style="" class="label"><rect></rect><foreignObject height="48" width="121.828125"><div style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"><p>⚙️ Device 2<br>Agents · Projects</p></span></div></foreignObject></g></g></g></g></g></svg></span></p>
<p>The system follows a <strong>heartbeat model</strong>.
Each coding agent session reports its state to the dashboard backend at regular intervals.
This report includes project information, git status, context usage, active <a class="post-link " title="Introduction to the Model Context Protocol (MCP): The Future of AI Integration" href="/model-context-protocol-mcp-introduction">MCP</a> servers, and the agent's current task.</p>
<p>The heartbeat data is gathered through <strong>coding agent hooks</strong>.
In the case of Claude Code, these are notification hooks that fire when the agent transitions between states (working, idle, awaiting permission).
The hooks execute a lightweight script that posts the session state to the dashboard API.</p>
<p>For terminal attachment, the backend establishes a <strong>WebSocket relay</strong> between the browser and the remote machine's terminal session.
Since the agent and the human connect to the same underlying session, they can interact with it simultaneously.</p>
<p>The design decision I'm happiest with is the <strong>enricher pattern</strong>.
The raw data coming from agent hooks is passed through a chain of enrichers, each responsible for extracting or deriving specific information.
For example, one enricher parses the agent's transcript to extract model name, token usage, and context percentage.
Another enricher detects PR URLs from the git branch.</p>
<p>This pattern is what makes the system <strong>extensible and agent-agnostic in the core dashboard</strong>, with agent-specific hooks and enrichers handling the adaptation layer.
The dashboard doesn't need to know the internals of any particular coding agent.
It only needs a hook script that sends heartbeats and an enricher that knows how to interpret the raw data.
Supporting a new CLI agent, whether <a class="post-link " title="Introducing Goose, the on-machine AI agent" href="/goose-on-machine-ai-agent-cli-introduction">Goose</a>, Gemini CLI, or any future tool, is mostly a matter of writing a new hook and a new enricher.
The rest of the system remains unchanged.</p>
<p>A quick note on scope: this is currently optimized for <strong>live orchestration and intervention</strong>, not long-term analytics.
The focus is real-time visibility and control across devices.
I can add historical views later, but the immediate win was reducing the cost of checking and steering active sessions.</p>
<h2 class="heading"><a href="https://blog.marcnuri.com/ai-coding-agent-dashboard#what-ive-learned" aria-label="what-ive-learned permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="what-ive-learned"></span>What I've Learned Running This</h2>
<p>After several weeks of daily use, a few insights stand out.</p>
<p><strong>Visibility reduces cognitive load dramatically.</strong>
The single biggest improvement isn't any specific feature.
It's simply being able to see everything at once.
Before the dashboard, I spent mental energy tracking session states in my head.
Now that state is externalized, I can focus on higher-level decisions like what to assign next and which results to review first.</p>
<p><strong>Context percentage is the most actionable metric.</strong>
Of all the fields on the dashboard, context usage has been the best predictor of where I should look next.
When an agent is running high on context, it usually means I need to review progress, reset with a fresh session, or prepare for handoff.</p>
<p><strong>PR awareness shortens the review loop.</strong>
Seeing which sessions have produced pull requests, with direct links, lets me move from orchestration to review immediately instead of hunting through <a class="tag-link " title="GitHub" aria-label="GitHub" href="/tag/github">GitHub</a> notifications.</p>
<p><strong>Remote access changes when and where you can be productive.</strong>
This sounds obvious in retrospect, but actually experiencing it is different from theorizing about it.
Being able to start a session from my phone during a coffee break and review the results from my workstation an hour later has added genuinely productive moments to parts of the day that were previously dead time.</p>
<h2 class="heading"><a href="https://blog.marcnuri.com/ai-coding-agent-dashboard#whats-next" aria-label="whats-next permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="whats-next"></span>What's Next</h2>
<p>The dashboard is functional and I use it daily, but the roadmap is driven by a clear goal: <strong>reducing the need to leave the dashboard for anything</strong>.</p>
<p>In the short term, I'm focused on features that eliminate round-trips to other tools:</p>
<ul>
<li><strong>Coding agent settings management</strong>: Configure agent behavior directly from the dashboard instead of editing configuration files on each machine.</li>
<li><strong>File attachment</strong>: Send files, context documents, or reference material to an agent session without switching to a terminal.</li>
<li><strong>Code diff viewer</strong>: Review the changes an agent has made without opening an IDE, closing a major gap in the orchestration workflow.</li>
</ul>
<p>On the extensibility front, <strong>support for additional coding agents</strong> is a natural next step.
The enricher pattern I described above was designed with this in mind.
As more CLI-based coding agents emerge, the dashboard should be able to aggregate sessions from all of them into the same unified view, regardless of which agent is running underneath.</p>
<p>Most importantly, I'm seriously considering <strong>open-sourcing</strong> the dashboard, or at least the key components: the hook scripts, the heartbeat protocol, and the enricher framework.
These pieces could be useful to anyone building similar orchestration workflows, and I'd love to see what others do with them.</p>
<p>If you're running parallel coding agents and have thoughts on what would make a tool like this more useful, I'd genuinely love to hear from you.
Whether it's a feature idea, a different approach to the same problem, or just your experience managing multiple agents, reach out.</p>
<h2 class="heading"><a href="https://blog.marcnuri.com/ai-coding-agent-dashboard#conclusion" aria-label="conclusion permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="conclusion"></span>Conclusion</h2>
<p>The shift from implementer to orchestrator that I described in my <a class="post-link " title="Boosting My Developer Productivity with AI in 2025" href="/boosting-developer-productivity-ai-2025">productivity post</a> requires different tools.
Standard terminals and IDEs were not built for one developer managing a fleet of AI agents across multiple machines.</p>
<p>This dashboard is my current answer to that gap.
It gives me visibility, control, and a consistent way to monitor, intervene, and start work from anywhere.</p>
<p>Developers who learn to orchestrate <a class="category-link " title="Artificial Intelligence: Everything related to artificial intelligence (AI) and machine learning (ML)" aria-label="AI" href="/category/ai">AI</a> agents effectively will have a real advantage in the next era of software engineering.
And the tools we build today to support that orchestration, however rough around the edges, will shape what that era looks like.</p>
<p>I'm still iterating on this, and I'd welcome feedback, feature suggestions, and stories from anyone tackling similar challenges.</p>
  ]]></content:encoded>
            <category>Artificial Intelligence</category>
            <enclosure url="https://blog.marcnuri.com/static/f6e7a03c8e367dfa85de34c8b4e87fd1/818f3/ai-coding-agent-dashboard.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Eclipse JKube 1.19 is now available!]]></title>
            <link>https://blog.marcnuri.com/eclipse-jkube-1-19</link>
            <guid isPermaLink="false">https://blog.marcnuri.com/eclipse-jkube-1-19</guid>
            <pubDate>Tue, 10 Feb 2026 14:00:00 GMT</pubDate>
            <description><![CDATA[Eclipse JKube 1.19 is available! Check out the major changes and learn how you can contribute.]]></description>
            <content:encoded><![CDATA[
    <div><a href="https://blog.marcnuri.com/eclipse-jkube-1-19">Original post</a></div>
    <p>On behalf of the <a class="post-link " title="Eclipse JKube introduction: Java tools and plugins for Kubernetes and OpenShift" href="/eclipse-jkube-introduction-kubernetes-openshift">Eclipse JKube</a>
team and everyone who has contributed, I'm happy to announce that Eclipse JKube <code>1.19.0</code> has been
<a href="https://github.com/eclipse-jkube/jkube/releases/tag/v1.19.0" rel="noopener" title="Link to https://github.com/eclipse-jkube/jkube/releases/tag/v1.19.0" aria-label="released" target="_blank">released</a> and is now available from
<a href="https://repo1.maven.org/maven2/org/eclipse/jkube/kubernetes-maven-plugin/1.19.0/" rel="noopener" title="Link to https://repo1.maven.org/maven2/org/eclipse/jkube/kubernetes-maven-plugin/1.19.0/" aria-label="Maven Central" target="_blank">Maven Central</a> 🎉.</p>
<p>Thanks to all of you who have contributed with issue reports, pull requests, feedback, and spreading the word
with blogs, videos, comments, and so on.
We really appreciate your help, keep it up!</p>
<h2 class="heading"><a href="https://blog.marcnuri.com/eclipse-jkube-1-19#whats-new" aria-label="whats-new permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="whats-new"></span>What's new?</h2>
<p>Without further ado, let's have a look at the most significant updates:</p>
<ul>
<li><a class="post-link " title="Link to the Spring Boot section" href="/eclipse-jkube-1-19#spring-boot">Improved Spring Boot health probes configuration</a></li>
<li><a class="post-link " title="Link to the ECR authentication section" href="/eclipse-jkube-1-19#ecr-auth">ECR registry authentication with AWS SDK v2</a></li>
<li><a class="post-link " title="Link to the Ingress section" href="/eclipse-jkube-1-19#ingress">IngressClassName support for Ingress resources</a></li>
<li>Updated base images from UBI 8 to UBI 9</li>
<li>Reduced dependencies (Guava removal)</li>
<li>🐛 Many other bug-fixes and minor improvements</li>
</ul>
<h3 class="heading"><a href="https://blog.marcnuri.com/eclipse-jkube-1-19#spring-boot" aria-label="spring-boot permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="spring-boot"></span>Improved Spring Boot health probes configuration</h3>
<p>This release brings several improvements for the Spring Boot health probes configuration:</p>
<ul>
<li>JKube now uses the correct <code>management.endpoint.health.probes.enabled</code> property. The previous property (<code>management.health.probes.enabled</code>) was deprecated in Spring Boot 2.3.2.</li>
<li>Added support for <code>server.ssl.enabled</code> and <code>management.server.ssl.enabled</code> properties to enable liveness/readiness probes for Spring Boot Actuator. This allows for easier environment-specific SSL configuration.</li>
</ul>
<h3 class="heading"><a href="https://blog.marcnuri.com/eclipse-jkube-1-19#ecr-auth" aria-label="ecr-auth permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="ecr-auth"></span>ECR registry authentication with AWS SDK v2</h3>
<p>JKube now supports Amazon ECR registry authentication using AWS SDK Java v2.
This update ensures compatibility with the latest AWS SDK and provides a more robust authentication mechanism
when pushing images to Amazon Elastic Container Registry.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/eclipse-jkube-1-19#ingress" aria-label="ingress permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="ingress"></span>IngressClassName support for Ingress resources</h3>
<p>The <code>IngressClassName</code> field is now supported in the <code>NetworkingV1IngressGenerator</code>.
This is essential for Kubernetes environments with multiple ingress controllers, allowing you to specify which ingress controller should handle your Ingress resources.</p>
<h2 class="heading"><a href="https://blog.marcnuri.com/eclipse-jkube-1-19#using-this-release" aria-label="using-this-release permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="using-this-release"></span>Using this release</h2>
<p>If your project is based on Maven, you just need to add the Kubernetes Maven plugin or the OpenShift Maven
plugin to your plugin dependencies:</p>
<div class="code" style="background:#232323"><pre style="display:block;overflow-x:auto;padding:0.5em;background:#232323;color:#e6e1dc"><code class="language-xml" style="white-space:pre"><span style="color:#e8bf6a">&lt;</span><span style="color:#e8bf6a">plugin</span><span style="color:#e8bf6a">&gt;</span><span>
</span><span>  </span><span style="color:#e8bf6a">&lt;</span><span style="color:#e8bf6a">groupId</span><span style="color:#e8bf6a">&gt;</span><span>org.eclipse.jkube</span><span style="color:#e8bf6a">&lt;/</span><span style="color:#e8bf6a">groupId</span><span style="color:#e8bf6a">&gt;</span><span>
</span><span>  </span><span style="color:#e8bf6a">&lt;</span><span style="color:#e8bf6a">artifactId</span><span style="color:#e8bf6a">&gt;</span><span>kubernetes-maven-plugin</span><span style="color:#e8bf6a">&lt;/</span><span style="color:#e8bf6a">artifactId</span><span style="color:#e8bf6a">&gt;</span><span>
</span><span>  </span><span style="color:#e8bf6a">&lt;</span><span style="color:#e8bf6a">version</span><span style="color:#e8bf6a">&gt;</span><span>1.19.0</span><span style="color:#e8bf6a">&lt;/</span><span style="color:#e8bf6a">version</span><span style="color:#e8bf6a">&gt;</span><span>
</span><span></span><span style="color:#e8bf6a">&lt;/</span><span style="color:#e8bf6a">plugin</span><span style="color:#e8bf6a">&gt;</span></code></pre></div>
<p>If your project is based on Gradle, you just need to add the Kubernetes Gradle plugin or the OpenShift Gradle
plugin to your plugin dependencies:</p>
<div class="code" style="background:#232323"><pre style="display:block;overflow-x:auto;padding:0.5em;background:#232323;color:#e6e1dc"><code class="language-groovy" style="white-space:pre"><span>plugins {
</span><span>  id </span><span style="color:#a5c261">'org.eclipse.jkube.kubernetes'</span><span> version </span><span style="color:#a5c261">'1.19.0'</span><span>
</span>}</code></pre></div>
<h2 class="heading"><a href="https://blog.marcnuri.com/eclipse-jkube-1-19#how-can-you-help" aria-label="how-can-you-help permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="how-can-you-help"></span>How can you help?</h2>
<p>If you're interested in helping out and are a first-time contributor, check out the
<a href="https://github.com/eclipse-jkube/jkube/labels/first-timers-only" rel="noopener" title="Link to https://github.com/eclipse-jkube/jkube/labels/first-timers-only" aria-label="&quot;first-timers-only&quot;" target="_blank">"first-timers-only"</a>
tag in the issue repository.
We've tagged extremely easy issues so that you can get started contributing to Open Source and the Eclipse organization.</p>
<p>If you are a more experienced developer or have already contributed to JKube, check the
<a href="https://github.com/eclipse-jkube/jkube/labels/help%20wanted" rel="noopener" title="Link to https://github.com/eclipse-jkube/jkube/labels/help%20wanted" aria-label="&quot;help wanted&quot;" target="_blank">"help wanted"</a> tag.</p>
<p>We're also excited to read articles and posts mentioning our project and sharing the user experience.
Feedback is the only way to improve.</p>
<p><a href="https://www.eclipse.org/jkube" rel="noopener" title="Link to https://www.eclipse.org/jkube" aria-label="Project Page" target="_blank">Project Page</a> |
<a href="https://github.com/eclipse-jkube/jkube" rel="noopener" title="Link to https://github.com/eclipse-jkube/jkube" aria-label="GitHub" target="_blank">GitHub</a> |
<a href="https://github.com/eclipse-jkube/jkube/issues" rel="noopener" title="Link to https://github.com/eclipse-jkube/jkube/issues" aria-label="Issues" target="_blank">Issues</a> |
<a href="https://gitter.im/eclipse/jkube" rel="noopener" title="Link to https://gitter.im/eclipse/jkube" aria-label="Gitter" target="_blank">Gitter</a> |
<a href="https://accounts.eclipse.org/mailing-list/jkube-dev" rel="noopener" title="Link to https://accounts.eclipse.org/mailing-list/jkube-dev" aria-label="Mailing list" target="_blank">Mailing list</a> |
<a href="https://stackoverflow.com/questions/tagged/jkube" rel="noopener" title="Link to https://stackoverflow.com/questions/tagged/jkube" aria-label="Stack Overflow" target="_blank">Stack Overflow</a></p>
<span class="post-image__pusher "></span><figure class="post-image "><span class="post-image__scrim"></span><a href="/static/af6673438e09cd98812665335c800cbf/e5189/JKube-blog-banner.jpg" class="post-image__link" title="The logo of Eclipse JKube"><span class="post-image__image-container"><div data-gatsby-image-wrapper="" class="gatsby-image-wrapper gatsby-image-wrapper-constrained post-image__image "><picture><source type="image/webp" data-srcset="/static/af6673438e09cd98812665335c800cbf/15be0/JKube-blog-banner.webp 200w,/static/af6673438e09cd98812665335c800cbf/f0f61/JKube-blog-banner.webp 400w,/static/af6673438e09cd98812665335c800cbf/eb6ca/JKube-blog-banner.webp 800w" sizes="(min-width: 800px) 800px, 100vw"><img data-gatsby-image-ssr="" data-main-image="" style="opacity: 1;" sizes="(min-width: 800px) 800px, 100vw" decoding="async" loading="lazy" data-src="/static/af6673438e09cd98812665335c800cbf/e5189/JKube-blog-banner.jpg" data-srcset="/static/af6673438e09cd98812665335c800cbf/f8ab9/JKube-blog-banner.jpg 200w,/static/af6673438e09cd98812665335c800cbf/a4161/JKube-blog-banner.jpg 400w,/static/af6673438e09cd98812665335c800cbf/e5189/JKube-blog-banner.jpg 800w" alt="The logo of Eclipse JKube" src="https://blog.marcnuri.com/static/af6673438e09cd98812665335c800cbf/e5189/JKube-blog-banner.jpg" srcset="https://blog.marcnuri.com/static/af6673438e09cd98812665335c800cbf/f8ab9/JKube-blog-banner.jpg 200w,https://blog.marcnuri.com/static/af6673438e09cd98812665335c800cbf/a4161/JKube-blog-banner.jpg 400w,https://blog.marcnuri.com/static/af6673438e09cd98812665335c800cbf/e5189/JKube-blog-banner.jpg 800w"></picture><script type="module">const t="undefined"!=typeof HTMLImageElement&&"loading"in HTMLImageElement.prototype;if(t){const t=document.querySelectorAll("img[data-main-image]");for(let e of t){e.dataset.src&&(e.setAttribute("src",e.dataset.src),e.removeAttribute("data-src")),e.dataset.srcset&&(e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset"));const t=e.parentNode.querySelectorAll("source[data-srcset]");for(let e of t)e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset");e.complete&&(e.style.opacity=1,e.parentNode.parentNode.querySelector("[data-placeholder-image]").style.opacity=0)}}</script></div></span></a></figure>
  ]]></content:encoded>
            <category>Cloud Native</category>
            <enclosure url="https://blog.marcnuri.com/static/af6673438e09cd98812665335c800cbf/e5189/JKube-blog-banner.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[2025 Year in Review: The Year of AI]]></title>
            <link>https://blog.marcnuri.com/2025-year-in-review-the-year-of-ai</link>
            <guid isPermaLink="false">https://blog.marcnuri.com/2025-year-in-review-the-year-of-ai</guid>
            <pubDate>Wed, 28 Jan 2026 16:00:00 GMT</pubDate>
            <description><![CDATA[Personal reflections on 2025, my sixth year as a Red Hatter, and the year AI transformed my developer workflow.]]></description>
            <content:encoded><![CDATA[
    <div><a href="https://blog.marcnuri.com/2025-year-in-review-the-year-of-ai">Original post</a></div>
    <span class="post-image__pusher "></span><figure class="post-image "><span class="post-image__scrim"></span><a href="/static/9e8f485fac9f407dfd5b866eba5a4a5c/4ca97/2025-year-of-ai-hero.jpg" class="post-image__link" title="2025: Year of AI"><span class="post-image__image-container"><div data-gatsby-image-wrapper="" class="gatsby-image-wrapper gatsby-image-wrapper-constrained post-image__image "><picture><source type="image/webp" data-srcset="/static/9e8f485fac9f407dfd5b866eba5a4a5c/066e1/2025-year-of-ai-hero.webp 625w,/static/9e8f485fac9f407dfd5b866eba5a4a5c/2034e/2025-year-of-ai-hero.webp 1250w,/static/9e8f485fac9f407dfd5b866eba5a4a5c/cb0df/2025-year-of-ai-hero.webp 2500w" sizes="(min-width: 2500px) 2500px, 100vw"><img data-gatsby-image-ssr="" data-main-image="" style="opacity: 1;" sizes="(min-width: 2500px) 2500px, 100vw" decoding="async" loading="lazy" data-src="/static/9e8f485fac9f407dfd5b866eba5a4a5c/4ca97/2025-year-of-ai-hero.jpg" data-srcset="/static/9e8f485fac9f407dfd5b866eba5a4a5c/fe964/2025-year-of-ai-hero.jpg 625w,/static/9e8f485fac9f407dfd5b866eba5a4a5c/6cf10/2025-year-of-ai-hero.jpg 1250w,/static/9e8f485fac9f407dfd5b866eba5a4a5c/4ca97/2025-year-of-ai-hero.jpg 2500w" alt="2025: Year of AI" src="https://blog.marcnuri.com/static/9e8f485fac9f407dfd5b866eba5a4a5c/4ca97/2025-year-of-ai-hero.jpg" srcset="https://blog.marcnuri.com/static/9e8f485fac9f407dfd5b866eba5a4a5c/fe964/2025-year-of-ai-hero.jpg 625w,https://blog.marcnuri.com/static/9e8f485fac9f407dfd5b866eba5a4a5c/6cf10/2025-year-of-ai-hero.jpg 1250w,https://blog.marcnuri.com/static/9e8f485fac9f407dfd5b866eba5a4a5c/4ca97/2025-year-of-ai-hero.jpg 2500w"></picture><script type="module">const t="undefined"!=typeof HTMLImageElement&&"loading"in HTMLImageElement.prototype;if(t){const t=document.querySelectorAll("img[data-main-image]");for(let e of t){e.dataset.src&&(e.setAttribute("src",e.dataset.src),e.removeAttribute("data-src")),e.dataset.srcset&&(e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset"));const t=e.parentNode.querySelectorAll("source[data-srcset]");for(let e of t)e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset");e.complete&&(e.style.opacity=1,e.parentNode.parentNode.querySelector("[data-placeholder-image]").style.opacity=0)}}</script></div></span></a></figure>
<p>2025 was a turning point.
It was the year <a class="category-link " title="Artificial Intelligence: Everything related to artificial intelligence (AI) and machine learning (ML)" aria-label="artificial intelligence" href="/category/ai">artificial intelligence</a> fundamentally transformed how I work, think, and build software.</p>
<p>This is my sixth <a class="tag-link " title="RedHattiversary" aria-label="Red Hat anniversary" href="/tag/redhattiversary">Red Hat anniversary</a> post, but I'm reframing the series.
Rather than focusing solely on my work anniversary in late October, I want to reflect on the entire year and the profound changes it brought.</p>
<p>You can catch up on my previous years as a <a class="tag-link " title="Red Hat" aria-label="Red Hatter" href="/tag/red-hat">Red Hatter</a> in these posts:</p>
<ul>
<li><a class="post-link " title="A year at Red Hat" href="/a-year-at-red-hat">My first year at Red Hat</a></li>
<li><a class="post-link " title="Two years at Red Hat" href="/two-years-at-redhat">Two years of Open Source at Red Hat</a></li>
<li><a class="post-link " title="Three years at Red Hat" href="/three-years-at-redhat">Reflecting on three years at Red Hat</a></li>
<li><a class="post-link " title="Four years at Red Hat" href="/four-years-at-red-hat">Four years of Free Software at Red Hat</a></li>
<li><a class="post-link " title="Five years at Red Hat" href="/five-years-at-red-hat">Five years at Red Hat</a></li>
</ul>
<p><em>📷 Featured photo by <a href="https://www.elartedejulieta.com" rel="noopener" title="Link to https://www.elartedejulieta.com" aria-label="Julia (El Arte de Julieta)" target="_blank">Julia (El Arte de Julieta)</a></em></p>
<h2 class="heading"><a href="https://blog.marcnuri.com/2025-year-in-review-the-year-of-ai#a-pivotal-year" aria-label="a-pivotal-year permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="a-pivotal-year"></span>A pivotal year</h2>
<p>If I had to choose one word to describe 2025, it would be <strong>pivotal</strong>.</p>
<p>On one hand, this was the year I fully embraced AI-assisted development.
I <a class="post-link " title="Boosting My Developer Productivity with AI in 2025" href="/boosting-developer-productivity-ai-2025">restructured my workflows around AI agents</a>, achieved significant productivity gains, and created tooling that bridges the gap between AI systems and cloud-native infrastructure.</p>
<p>On the other hand, it was also a year of uncertainty.
In February 2025, Red Hat <a href="https://www.redhat.com/en/blog/evolving-our-middleware-strategy" rel="noopener" title="Link to https://www.redhat.com/en/blog/evolving-our-middleware-strategy" aria-label="announced changes to its middleware strategy" target="_blank">announced changes to its middleware strategy</a>, transferring key teams to IBM.
These were the teams that relied most heavily on <a class="tag-link " title="Fabric8" aria-label="Fabric8 Kubernetes Client" href="/tag/fabric8">Fabric8 Kubernetes Client</a> and <a class="tag-link " title="Eclipse JKube" aria-label="Eclipse JKube" href="/tag/jkube">Eclipse JKube</a>, the projects I've poured years of effort into.</p>
<p>Despite my respect for IBM, it weighed on me.
<a class="tag-link " title="RedHattiversary" aria-label="Being a Red Hatter" href="/tag/redhattiversary">Being a Red Hatter</a> isn't just a job title for me; it's an identity I've cherished since my teenage years when I first discovered Red Hat Linux.</p>
<p>But here I am, still doing what I love: building <a class="tag-link " title="Open Source" aria-label="free open source" href="/tag/open-source">free open source</a> tools that help developers.
And this year, that mission expanded in unexpected directions.</p>
<h2 class="heading"><a href="https://blog.marcnuri.com/2025-year-in-review-the-year-of-ai#this-years-highlights" aria-label="this-years-highlights permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="this-years-highlights"></span>This year's highlights</h2>
<p>Now let me share some of the highlights from 2025.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/2025-year-in-review-the-year-of-ai#projects" aria-label="projects permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="projects"></span>Projects</h3>
<h4 class="heading"><a href="https://blog.marcnuri.com/2025-year-in-review-the-year-of-ai#kubernetes-mcp-server" aria-label="kubernetes-mcp-server permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="kubernetes-mcp-server"></span>Kubernetes MCP Server</h4>
<p>If there's one project that defined 2025 for me, it's the <a href="https://github.com/containers/kubernetes-mcp-server" rel="noopener" title="Link to https://github.com/containers/kubernetes-mcp-server" aria-label="Kubernetes MCP Server" target="_blank">Kubernetes MCP Server</a>.</p>
<p>It started as a proof of concept in late January 2025, originally built around the <a class="post-link " title="Kubernetes Client for Java: Fabric8 introduction" href="/kubernetes-client-java-fabric8-introduction">Fabric8 Kubernetes Client</a>.
When I first ran the MCP server, the results surprised me.
It was the first time I truly realized the potential of AI.
Until then, AI was cool but not that much of a game changer.
Seeing an AI agent autonomously deploy and manage applications on Kubernetes was a revelation.</p>
<p>I sent a few demos to internal mailing lists at Red Hat, and they were a great success.
Seeing the potential, I decided to port the project to Go.
Most of the Kubernetes ecosystem is built in Go, and Kubernetes developers are far more familiar with it than Java.
This turned out to be the right call.
Adoption started to increase, and so did interest from across Red Hat.
People from other business units reached out to learn more and explore how they could use it.</p>
<p>I put a lot of effort into promoting the project and making it stable enough to be embedded in real products.
The <a class="post-link " title="Introduction to the Model Context Protocol (MCP): The Future of AI Integration" href="/model-context-protocol-mcp-introduction">Model Context Protocol (MCP)</a> provided the perfect bridge between AI agents and Kubernetes clusters, allowing AI systems to deploy, manage, and troubleshoot applications autonomously.</p>
<p>The project's growth speaks for itself: over <strong>1,050 stars</strong> and <strong>232 forks</strong> on GitHub, reflecting both its adoption by the AI developer community and the growing number of contributors.</p>
<p>The peak came when the project <a class="post-link " title="Kubernetes MCP Server Joins the Containers Organization!" href="/kubernetes-mcp-server-containers-organization">joined the Containers organization</a> on GitHub, alongside industry-standard tools like Podman and Buildah.
This gave the project a neutral space for collaboration and validated its importance.
At this point, teams from OpenShift are actively contributing features on top of it.</p>
<p>This is one of the biggest successes of my career at Red Hat.
Taking a proof of concept from my laptop to an organization-level project with cross-team contributions in less than a year is something I'm incredibly proud of.</p>
<p>Along the way, I documented my learnings on this blog.
From <a class="post-link " title="Giving Superpowers to Small Language Models with Model Context Protocol (MCP)" href="/giving-superpowers-to-small-language-models-with-mcp">giving superpowers to small language models</a> to <a class="post-link " title="Connecting to a Model Context Protocol (MCP) Server from Java using LangChain4j" href="/connecting-to-mcp-server-with-langchain4j">connecting MCP servers with various AI frameworks</a>, these posts capture the evolution of my thinking about <a class="post-link " title="The Future of Developer Tools: Adapting to Machine-Based Developers" href="/the-future-of-developer-tools-machine-based-developers">the future of developer tools</a> and how they need to evolve for AI-augmented development.</p>
<h4 class="heading"><a href="https://blog.marcnuri.com/2025-year-in-review-the-year-of-ai#fabric8-kubernetes-client" aria-label="fabric8-kubernetes-client permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="fabric8-kubernetes-client"></span>Fabric8 Kubernetes Client</h4>
<p>The <a class="tag-link " title="Fabric8" aria-label="Fabric8" href="/tag/fabric8">Fabric8</a> Kubernetes Client saw <strong>7 releases</strong> this year, including the new 7.x line.
We released versions 7.1.0 through 7.4.0, plus maintenance releases for the 6.x branch.</p>
<p>Community contributions remain strong, and the project continues to be a foundation for countless Kubernetes tools in the Java ecosystem.
While my personal focus shifted toward MCP tooling, the project remains healthy and actively maintained.</p>
<p>I'm happy that <a href="https://www.linkedin.com/in/ashish-thakur111" rel="noopener" title="Link to https://www.linkedin.com/in/ashish-thakur111" aria-label="Ashish Thakur" target="_blank">Ashish Thakur</a> joined the team this year to help with the maintenance of both Fabric8 and JKube.
His involvement ensures these projects continue to thrive.</p>
<h4 class="heading"><a href="https://blog.marcnuri.com/2025-year-in-review-the-year-of-ai#eclipse-jkube" aria-label="eclipse-jkube permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="eclipse-jkube"></span>Eclipse JKube</h4>
<p><a class="tag-link " title="Eclipse JKube" aria-label="Eclipse JKube" href="/tag/jkube">Eclipse JKube</a> had <strong>3 releases</strong> this year (1.18.0, 1.18.1, 1.18.2).
The project now has <strong>846 stars</strong> and <strong>551 forks</strong>.</p>
<p>With the middleware team changes, the immediate future of JKube felt uncertain at times.
But with Ashish on board and continued community growth, the project remains valuable for Java developers deploying to Kubernetes.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/2025-year-in-review-the-year-of-ai#the-ai-transformation" aria-label="the-ai-transformation permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="the-ai-transformation"></span>The AI Transformation</h3>
<p>Beyond building MCP servers, 2025 was the year I transformed my own development practices.</p>
<p>After my summer break, I went all-in on AI tooling.
I wrote about this extensively in <a class="post-link " title="Boosting My Developer Productivity with AI in 2025" href="/boosting-developer-productivity-ai-2025">Boosting My Developer Productivity with AI in 2025</a>, but the short version is: my productivity more than doubled.</p>
<p>The key insight isn't about any single tool, but about <strong>parallelism</strong>.
Using CLI agents like Claude Code and GitHub's Copilot Coding Agent, I can orchestrate multiple AI systems working on different tasks simultaneously.
My role has shifted from implementer to orchestrator.
I've since taken this further by <a class="post-link " title="AI Coding Agent Dashboard: Orchestrating Claude Code Across Devices" href="/ai-coding-agent-dashboard">building a dashboard to monitor and orchestrate these agent sessions</a> across all my devices in real-time.</p>
<p>This shift had uncomfortable implications too.
The nature of the job is changing.
<a class="post-link " title="Boosting My Developer Productivity with AI in 2025" href="/boosting-developer-productivity-ai-2025#coding-is-no-longer-the-job">Coding is no longer the job</a>; orchestrating AI agents is.
As someone who genuinely loves the craft of coding, this realization was bittersweet.</p>
<p>I'm confident that software engineers will still be needed, even if our roles evolve toward orchestration and architecture.
This shift doesn't diminish the importance of engineering fundamentals; if anything, it amplifies them.
What worries me is that decision-makers who don't understand the nuances might believe that engineering teams can be fully replaced by AI agents.
That's a dangerous misconception, and one I hope the industry navigates carefully.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/2025-year-in-review-the-year-of-ai#public-speaking" aria-label="public-speaking permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="public-speaking"></span>Public Speaking</h3>
<p>This year I spoke at <strong><a class="tag-link " title="DevBcn" aria-label="DevBcn" href="/tag/devbcn">DevBcn</a> 2025</strong> in July, delivering a talk on <strong>"Model Context Protocol Servers 101: Unlocking the Power of AI"</strong>.
It was a comprehensive introduction to MCP and its implications for the future of developer tooling.</p>
<p>To my surprise, I received the <strong>"Most Original Speaker"</strong> award.
Once again, I'm humbled by this recognition.
There were many great talks at the conference, and in my opinion, several deserved the award more than I did.
However, for an introvert like me, this is huge.
It's a testament to the effort I put into each of my conference talks, and validation that stepping outside my comfort zone has been worth it.</p>
<p>Unfortunately, the talk wasn't officially recorded.
I did record a version from home later that week, which you can watch below.
It's not as engaging as the live presentation, but if you're curious about MCP, it covers the essentials:</p>
<div><iframe src="https://www.youtube.com/embed/7Tpz5dG5k_w" class="aligncenter " width="560" height="315" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe></div>
<h2 class="heading"><a href="https://blog.marcnuri.com/2025-year-in-review-the-year-of-ai#looking-forward" aria-label="looking-forward permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="looking-forward"></span>Looking Forward</h2>
<p>As I write this in early 2026, the trajectory is clear: AI will continue reshaping how we build software.
The question isn't whether to embrace these changes, but how to do so thoughtfully.</p>
<p>I remain committed to building free open source tools that empower developers, whether those developers are humans, AI agents, or <a class="post-link " title="The Future of Developer Tools: Adapting to Machine-Based Developers" href="/the-future-of-developer-tools-machine-based-developers#shifting-perspectives-in-developer-tools">some combination of both</a>.
The Kubernetes MCP Server, Fabric8 Kubernetes Client, and Eclipse JKube will continue to evolve.</p>
<p>Despite the organizational uncertainties, I'm grateful to be doing what I love at Red Hat.
I want to thank my managers, who have reassured me about my value during these times.
Their support has meant a lot when things felt uncertain.</p>
<p>Six years in, and the mission remains the same: make developers' lives easier through free and open source software.</p>
<p>If you've followed my journey or used any of the tools I've worked on, I'd love to hear your thoughts.</p>
<p>At the end of the day, what drives me hasn't changed: the joy of building something useful, sharing it with the world, and seeing others build on top of it.
That's the magic of open source, and it's why I'll keep doing this for as long as I can.</p>
  ]]></content:encoded>
            <category>Personal</category>
            <enclosure url="https://blog.marcnuri.com/static/ace4b97c680b9cac0c41cf49743f2541/818f3/2025-year-of-ai.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Synology DS224+: How to upgrade hard drives in RAID 1]]></title>
            <link>https://blog.marcnuri.com/synology-nas-ds224-hard-drive-upgrade-raid1</link>
            <guid isPermaLink="false">https://blog.marcnuri.com/synology-nas-ds224-hard-drive-upgrade-raid1</guid>
            <pubDate>Tue, 13 Jan 2026 07:00:00 GMT</pubDate>
            <description><![CDATA[Step-by-step guide to upgrade hard drives on a Synology DS224+ NAS configured with RAID 1, increasing storage capacity without data loss.]]></description>
            <content:encoded><![CDATA[
    <div><a href="https://blog.marcnuri.com/synology-nas-ds224-hard-drive-upgrade-raid1">Original post</a></div>
    <h2 class="heading"><a href="https://blog.marcnuri.com/synology-nas-ds224-hard-drive-upgrade-raid1#introduction" aria-label="introduction permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="introduction"></span>Introduction</h2>
<p>The <a href="https://www.synology.com/en-us/products/DS224+" rel="noopener" title="Link to https://www.synology.com/en-us/products/DS224+" aria-label="Synology DiskStation DS224+" target="_blank">Synology DiskStation DS224+</a> is a compact 2-bay NAS designed for home users and small offices.
It's powered by an Intel Celeron J4125 quad-core processor (2.0 GHz, burst up to 2.7 GHz), comes with 2GB of DDR4 RAM (expandable to 6GB), and consumes only around 15 watts under full operation.
The DS224+ supports both Synology Hybrid RAID (SHR) and traditional RAID configurations (RAID 0/RAID 1), making it a versatile solution for centralized data storage and backup.</p>
<p>After some time using the NAS, I decided to upgrade the hard drives to increase the storage capacity.
The great thing about RAID 1 (mirroring) is that you can replace the drives one at a time, allowing the array to rebuild between replacements, which means <strong>no data loss</strong> and <strong>no downtime</strong> during the upgrade process.</p>
<p>This post documents the step-by-step procedure I followed to upgrade from my original drives to 2x 16TB drives, effectively increasing my usable storage capacity.</p>
<h2 class="heading"><a href="https://blog.marcnuri.com/synology-nas-ds224-hard-drive-upgrade-raid1#prerequisites" aria-label="prerequisites permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="prerequisites"></span>Prerequisites</h2>
<p>Before you begin, make sure you have:</p>
<ul>
<li><strong>Backup your data</strong>: Even though RAID 1 provides redundancy, always have a backup before any hardware changes</li>
<li><strong>New drives</strong>: 2x <a href="https://www.seagate.com/products/nas-drives/ironwolf-hard-drive/" rel="noopener" title="Link to https://www.seagate.com/products/nas-drives/ironwolf-hard-drive/" aria-label="Seagate IronWolf Pro 16TB" target="_blank">Seagate IronWolf Pro 16TB</a> (ST16000NT001) - 7200 RPM, CMR, 256MB cache</li>
<li><strong>Check drive compatibility</strong>: Verify your drives are on <a href="https://www.synology.com/compatibility" rel="noopener" title="Link to https://www.synology.com/compatibility" aria-label="Synology's compatibility list" target="_blank">Synology's compatibility list</a></li>
<li><strong>Run data scrubbing</strong>: Perform a data scrubbing operation before starting (see below)</li>
<li><strong>Time</strong>: The rebuild process can take several hours per drive</li>
<li><strong>Access to DSM</strong>: Ensure you can access your NAS web interface</li>
</ul>
<p>One of the prerequisites above deserves special attention: <strong>data scrubbing</strong>.
If you're not familiar with this operation, here's what you need to know.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/synology-nas-ds224-hard-drive-upgrade-raid1#what-is-data-scrubbing" aria-label="what-is-data-scrubbing permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="what-is-data-scrubbing"></span>What is data scrubbing?</h3>
<p><a href="https://kb.synology.com/en-us/DSM/help/DSM/StorageManager/storage_pool_data_scrubbing" rel="noopener" title="Link to https://kb.synology.com/en-us/DSM/help/DSM/StorageManager/storage_pool_data_scrubbing" aria-label="Data scrubbing" target="_blank">Data scrubbing</a> is a maintenance operation that scans your storage pool to detect and repair data inconsistencies before they become a problem.
It's essentially a health check for your drives and file system.</p>
<p>There are two types of scrubbing:</p>
<ul>
<li><strong>RAID scrubbing</strong>: Verifies that all parity data is consistent across the array. This is only available for SHR (with 3+ drives), RAID 5, RAID 6, or RAID F1. For a 2-drive RAID 1 setup, this type of scrubbing is not available.</li>
<li><strong>File system scrubbing</strong>: Available for Btrfs volumes, this uses checksums to verify data integrity and can repair corrupted data using redundant copies.</li>
</ul>
<p>Running data scrubbing before a drive upgrade ensures that your existing data is in a healthy state before you start replacing drives.
If there are any issues, it's better to discover and fix them while you still have both original drives in place.</p>
<p>To run data scrubbing:</p>
<ol>
<li>Open <strong>Storage Manager</strong> in DSM</li>
<li>Go to <strong>Storage Pool</strong></li>
<li>Select your pool to expand its details</li>
<li>Under the <strong>Data Scrubbing</strong> section, click <strong>Run Now</strong></li>
</ol>
<div class="admonition admonition__note"><p class="admonition-title"><i class="admonition-title-icon fa-solid fa-circle-info"></i>Note</p><div class="admonition-content">
<p>Data scrubbing can take several hours depending on your storage size. In my case, it took about <strong>9 hours</strong> for ~6TB of data.
It's recommended to run it during off-peak hours.
Synology recommends running data scrubbing <a href="https://blog.synology.com/how-data-scrubbing-protects-against-data-corruption" rel="noopener" title="Link to https://blog.synology.com/how-data-scrubbing-protects-against-data-corruption" aria-label="every 6 months" target="_blank">every 6 months</a> as part of regular maintenance.</p>
</div></div>
<h2 class="heading"><a href="https://blog.marcnuri.com/synology-nas-ds224-hard-drive-upgrade-raid1#current-setup" aria-label="current-setup permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="current-setup"></span>Current setup</h2>
<p>Before starting the upgrade, here's my current configuration:</p>
<ul>
<li><strong>NAS Model</strong>: Synology DS224+</li>
<li><strong>Current drives</strong>: 2x <a href="https://global.synologydownload.com/download/Document/Hardware/ProductSpec/Hard%20Drives/23-year/HAT3300-6T/enu/Product_Spec_HAT3300-6T_enu.pdf" rel="noopener" title="Link to https://global.synologydownload.com/download/Document/Hardware/ProductSpec/Hard%20Drives/23-year/HAT3300-6T/enu/Product_Spec_HAT3300-6T_enu.pdf" aria-label="Synology HAT3300-6T" target="_blank">Synology HAT3300-6T</a> (6TB, 5400 RPM, SATA III)</li>
<li><strong>Storage pool type</strong>: SHR (Synology Hybrid RAID) / RAID 1</li>
<li><strong>File system</strong>: Btrfs</li>
<li><strong>Total raw capacity</strong>: 12TB (2x 6TB)</li>
<li><strong>Usable capacity</strong>: 5.4TB (mirrored)</li>
</ul>
<p>The Synology HAT3300 Plus series drives are specifically designed for NAS use, featuring a 1 million hour MTTF and a workload rating of 180TB/year.
They've been reliable drives, but it's time for more storage space.</p>
<h2 class="heading"><a href="https://blog.marcnuri.com/synology-nas-ds224-hard-drive-upgrade-raid1#upgrade-procedure" aria-label="upgrade-procedure permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="upgrade-procedure"></span>Upgrade procedure</h2>
<p>The upgrade process involves replacing one drive at a time.
For each drive, you'll deactivate it, physically swap it, then repair and expand the storage pool.
The DS224+ supports hot swapping, so you don't need to power off the NAS, though you can do so if you prefer.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/synology-nas-ds224-hard-drive-upgrade-raid1#step-1-identify-drive" aria-label="step-1-identify-drive permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="step-1-identify-drive"></span>Step 1: Identify the drive to replace</h3>
<p>First, identify which drive you want to replace:</p>
<ol>
<li>Open <strong>Storage Manager</strong> in DSM</li>
<li>Select <strong>Storage</strong> from the left menu</li>
<li>Under <strong>Drive Info</strong>, note which drive number corresponds to each drive</li>
</ol>
<p>In my case, I'll start by replacing <strong>Drive 1</strong> as shown in the screenshot below.</p>
<span class="post-image__pusher "></span><figure class="post-image "><span class="post-image__scrim"></span><a href="/static/b445c24f8d7cc49fed14cbef2655b97e/96440/drive-info.png" class="post-image__link" title="Storage Manager Drive Info"><span class="post-image__image-container"><div data-gatsby-image-wrapper="" class="gatsby-image-wrapper gatsby-image-wrapper-constrained post-image__image "><picture><source type="image/webp" data-srcset="/static/b445c24f8d7cc49fed14cbef2655b97e/a8efb/drive-info.webp 216w,/static/b445c24f8d7cc49fed14cbef2655b97e/b8a6d/drive-info.webp 432w,/static/b445c24f8d7cc49fed14cbef2655b97e/ac750/drive-info.webp 863w" sizes="(min-width: 863px) 863px, 100vw"><img data-gatsby-image-ssr="" data-main-image="" style="opacity: 1;" sizes="(min-width: 863px) 863px, 100vw" decoding="async" loading="lazy" data-src="/static/b445c24f8d7cc49fed14cbef2655b97e/96440/drive-info.png" data-srcset="/static/b445c24f8d7cc49fed14cbef2655b97e/69b86/drive-info.png 216w,/static/b445c24f8d7cc49fed14cbef2655b97e/f8952/drive-info.png 432w,/static/b445c24f8d7cc49fed14cbef2655b97e/96440/drive-info.png 863w" alt="Storage Manager Drive Info" src="https://blog.marcnuri.com/static/b445c24f8d7cc49fed14cbef2655b97e/96440/drive-info.png" srcset="https://blog.marcnuri.com/static/b445c24f8d7cc49fed14cbef2655b97e/69b86/drive-info.png 216w,https://blog.marcnuri.com/static/b445c24f8d7cc49fed14cbef2655b97e/f8952/drive-info.png 432w,https://blog.marcnuri.com/static/b445c24f8d7cc49fed14cbef2655b97e/96440/drive-info.png 863w"></picture><script type="module">const t="undefined"!=typeof HTMLImageElement&&"loading"in HTMLImageElement.prototype;if(t){const t=document.querySelectorAll("img[data-main-image]");for(let e of t){e.dataset.src&&(e.setAttribute("src",e.dataset.src),e.removeAttribute("data-src")),e.dataset.srcset&&(e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset"));const t=e.parentNode.querySelectorAll("source[data-srcset]");for(let e of t)e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset");e.complete&&(e.style.opacity=1,e.parentNode.parentNode.querySelector("[data-placeholder-image]").style.opacity=0)}}</script></div></span></a></figure>
<p>With the drive identified, the next step is to locate its physical position in the NAS.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/synology-nas-ds224-hard-drive-upgrade-raid1#step-2-locate-drive" aria-label="step-2-locate-drive permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="step-2-locate-drive"></span>Step 2: Locate the physical drive</h3>
<p>Before removing anything, confirm which physical bay contains the drive:</p>
<ol>
<li>Go to <strong>Storage Manager</strong> &gt; <strong>HDD/SSD</strong></li>
<li>Select the drive you want to replace (e.g., Drive 1)</li>
<li>Click <strong>Locate Drive</strong></li>
</ol>
<p>The drive bay LED will change from green to amber for about a minute, as shown below.
This prevents accidentally removing the wrong drive.</p>
<span class="post-image__pusher "></span><figure class="post-image "><span class="post-image__scrim"></span><a href="/static/86d186c7b2ecc545ed088df987e80f25/e5e83/locate-drive.png" class="post-image__link" title="Locate Drive option in HDD/SSD"><span class="post-image__image-container"><div data-gatsby-image-wrapper="" class="gatsby-image-wrapper gatsby-image-wrapper-constrained post-image__image "><picture><source type="image/webp" data-srcset="/static/86d186c7b2ecc545ed088df987e80f25/befc6/locate-drive.webp 236w,/static/86d186c7b2ecc545ed088df987e80f25/8bd8c/locate-drive.webp 473w,/static/86d186c7b2ecc545ed088df987e80f25/6d5cf/locate-drive.webp 945w" sizes="(min-width: 945px) 945px, 100vw"><img data-gatsby-image-ssr="" data-main-image="" style="opacity: 1;" sizes="(min-width: 945px) 945px, 100vw" decoding="async" loading="lazy" data-src="/static/86d186c7b2ecc545ed088df987e80f25/e5e83/locate-drive.png" data-srcset="/static/86d186c7b2ecc545ed088df987e80f25/13f79/locate-drive.png 236w,/static/86d186c7b2ecc545ed088df987e80f25/1e2a9/locate-drive.png 473w,/static/86d186c7b2ecc545ed088df987e80f25/e5e83/locate-drive.png 945w" alt="Locate Drive option in HDD/SSD" src="https://blog.marcnuri.com/static/86d186c7b2ecc545ed088df987e80f25/e5e83/locate-drive.png" srcset="https://blog.marcnuri.com/static/86d186c7b2ecc545ed088df987e80f25/13f79/locate-drive.png 236w,https://blog.marcnuri.com/static/86d186c7b2ecc545ed088df987e80f25/1e2a9/locate-drive.png 473w,https://blog.marcnuri.com/static/86d186c7b2ecc545ed088df987e80f25/e5e83/locate-drive.png 945w"></picture><script type="module">const t="undefined"!=typeof HTMLImageElement&&"loading"in HTMLImageElement.prototype;if(t){const t=document.querySelectorAll("img[data-main-image]");for(let e of t){e.dataset.src&&(e.setAttribute("src",e.dataset.src),e.removeAttribute("data-src")),e.dataset.srcset&&(e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset"));const t=e.parentNode.querySelectorAll("source[data-srcset]");for(let e of t)e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset");e.complete&&(e.style.opacity=1,e.parentNode.parentNode.querySelector("[data-placeholder-image]").style.opacity=0)}}</script></div></span></a></figure>
<p>Once you've confirmed which bay contains the drive, you can proceed to deactivate it.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/synology-nas-ds224-hard-drive-upgrade-raid1#step-3-deactivate-drive" aria-label="step-3-deactivate-drive permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="step-3-deactivate-drive"></span>Step 3: Deactivate the drive</h3>
<p>Deactivating the drive before removal is the safest approach:</p>
<ol>
<li>With the drive still selected in <strong>HDD/SSD</strong></li>
<li>Click the <strong>Action</strong> menu</li>
<li>Select <strong>Deactivate Drive</strong></li>
<li>Confirm when prompted</li>
</ol>
<span class="post-image__pusher "></span><figure class="post-image "><span class="post-image__scrim"></span><a href="/static/b139ab882afb038fb86e4312c0216344/61124/deactivate-drive.png" class="post-image__link" title="Deactivate Drive confirmation"><span class="post-image__image-container"><div data-gatsby-image-wrapper="" class="gatsby-image-wrapper gatsby-image-wrapper-constrained post-image__image "><picture><source type="image/webp" data-srcset="/static/b139ab882afb038fb86e4312c0216344/685e6/deactivate-drive.webp 237w,/static/b139ab882afb038fb86e4312c0216344/a9a24/deactivate-drive.webp 474w,/static/b139ab882afb038fb86e4312c0216344/75f90/deactivate-drive.webp 947w" sizes="(min-width: 947px) 947px, 100vw"><img data-gatsby-image-ssr="" data-main-image="" style="opacity: 1;" sizes="(min-width: 947px) 947px, 100vw" decoding="async" loading="lazy" data-src="/static/b139ab882afb038fb86e4312c0216344/61124/deactivate-drive.png" data-srcset="/static/b139ab882afb038fb86e4312c0216344/136c3/deactivate-drive.png 237w,/static/b139ab882afb038fb86e4312c0216344/e3996/deactivate-drive.png 474w,/static/b139ab882afb038fb86e4312c0216344/61124/deactivate-drive.png 947w" alt="Deactivate Drive confirmation" src="https://blog.marcnuri.com/static/b139ab882afb038fb86e4312c0216344/61124/deactivate-drive.png" srcset="https://blog.marcnuri.com/static/b139ab882afb038fb86e4312c0216344/136c3/deactivate-drive.png 237w,https://blog.marcnuri.com/static/b139ab882afb038fb86e4312c0216344/e3996/deactivate-drive.png 474w,https://blog.marcnuri.com/static/b139ab882afb038fb86e4312c0216344/61124/deactivate-drive.png 947w"></picture><script type="module">const t="undefined"!=typeof HTMLImageElement&&"loading"in HTMLImageElement.prototype;if(t){const t=document.querySelectorAll("img[data-main-image]");for(let e of t){e.dataset.src&&(e.setAttribute("src",e.dataset.src),e.removeAttribute("data-src")),e.dataset.srcset&&(e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset"));const t=e.parentNode.querySelectorAll("source[data-srcset]");for(let e of t)e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset");e.complete&&(e.style.opacity=1,e.parentNode.parentNode.querySelector("[data-placeholder-image]").style.opacity=0)}}</script></div></span></a></figure>
<p>After confirmation, the storage pool will enter a <strong>degraded</strong> state and the NAS will start beeping.
Don't worry - this is expected behavior and we'll address it in the next step.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/synology-nas-ds224-hard-drive-upgrade-raid1#step-4-mute-beep" aria-label="step-4-mute-beep permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="step-4-mute-beep"></span>Step 4: Mute the beep alert</h3>
<p>To stop the beeping:</p>
<ol>
<li>Open <strong>Control Panel</strong></li>
<li>Go to <strong>Hardware &amp; Power</strong></li>
<li>Click the <strong>Mute</strong> button</li>
</ol>
<p>The NAS is designed to alert you when the storage pool is degraded - this is expected behavior during the upgrade.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/synology-nas-ds224-hard-drive-upgrade-raid1#step-5-replace-drive" aria-label="step-5-replace-drive permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="step-5-replace-drive"></span>Step 5: Replace the physical drive</h3>
<p>Now swap the drive:</p>
<ol>
<li>The DS224+ supports <strong>hot swapping</strong>, so you can replace the drive without powering off<!-- -->
<ul>
<li>If you prefer, you can power off the NAS first via <strong>Control Panel</strong> &gt; <strong>Hardware &amp; Power</strong> &gt; <strong>Shutdown</strong></li>
</ul>
</li>
<li>Remove the deactivated drive from its bay</li>
<li>Insert the new 16TB drive into the same bay</li>
<li>If you powered off, turn the NAS back on</li>
</ol>
<h3 class="heading"><a href="https://blog.marcnuri.com/synology-nas-ds224-hard-drive-upgrade-raid1#step-6-repair" aria-label="step-6-repair permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="step-6-repair"></span>Step 6: Repair the storage pool</h3>
<p>Once the new drive is detected, you need to initiate the repair process to rebuild the RAID array:</p>
<ol>
<li>Open <strong>Storage Manager</strong> &gt; <strong>Storage</strong></li>
<li>Click the <strong>three-dot menu</strong> (⋯) next to your storage pool</li>
<li>Select <strong>Repair</strong></li>
<li>In the repair window, select the new drive you just installed</li>
<li>Click <strong>Next</strong>, confirm the settings, and click <strong>Apply</strong></li>
</ol>
<p>The screenshot below shows the repair dialog where you select the newly installed drive.</p>
<span class="post-image__pusher "></span><figure class="post-image "><span class="post-image__scrim"></span><a href="/static/2da6e811fbc82fa9cfc649f8842aa6a9/e7929/repair-drive-1.png" class="post-image__link" title="Repair Storage Pool - Drive 1"><span class="post-image__image-container"><div data-gatsby-image-wrapper="" class="gatsby-image-wrapper gatsby-image-wrapper-constrained post-image__image "><picture><source type="image/webp" data-srcset="/static/2da6e811fbc82fa9cfc649f8842aa6a9/90fd2/repair-drive-1.webp 205w,/static/2da6e811fbc82fa9cfc649f8842aa6a9/c1f1e/repair-drive-1.webp 410w,/static/2da6e811fbc82fa9cfc649f8842aa6a9/a6c5e/repair-drive-1.webp 820w" sizes="(min-width: 820px) 820px, 100vw"><img data-gatsby-image-ssr="" data-main-image="" style="opacity: 1;" sizes="(min-width: 820px) 820px, 100vw" decoding="async" loading="lazy" data-src="/static/2da6e811fbc82fa9cfc649f8842aa6a9/e7929/repair-drive-1.png" data-srcset="/static/2da6e811fbc82fa9cfc649f8842aa6a9/19357/repair-drive-1.png 205w,/static/2da6e811fbc82fa9cfc649f8842aa6a9/bddad/repair-drive-1.png 410w,/static/2da6e811fbc82fa9cfc649f8842aa6a9/e7929/repair-drive-1.png 820w" alt="Repair Storage Pool - Drive 1" src="https://blog.marcnuri.com/static/2da6e811fbc82fa9cfc649f8842aa6a9/e7929/repair-drive-1.png" srcset="https://blog.marcnuri.com/static/2da6e811fbc82fa9cfc649f8842aa6a9/19357/repair-drive-1.png 205w,https://blog.marcnuri.com/static/2da6e811fbc82fa9cfc649f8842aa6a9/bddad/repair-drive-1.png 410w,https://blog.marcnuri.com/static/2da6e811fbc82fa9cfc649f8842aa6a9/e7929/repair-drive-1.png 820w"></picture><script type="module">const t="undefined"!=typeof HTMLImageElement&&"loading"in HTMLImageElement.prototype;if(t){const t=document.querySelectorAll("img[data-main-image]");for(let e of t){e.dataset.src&&(e.setAttribute("src",e.dataset.src),e.removeAttribute("data-src")),e.dataset.srcset&&(e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset"));const t=e.parentNode.querySelectorAll("source[data-srcset]");for(let e of t)e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset");e.complete&&(e.style.opacity=1,e.parentNode.parentNode.querySelector("[data-placeholder-image]").style.opacity=0)}}</script></div></span></a></figure>
<p>Once the repair starts, the system health will change from "Critical" to "Warning".</p>
<div class="admonition admonition__note"><p class="admonition-title"><i class="admonition-title-icon fa-solid fa-circle-info"></i>Note</p><div class="admonition-content">
<p>At this point, the storage pool capacity will <strong>not</strong> increase yet.
In RAID 1, the usable capacity is limited by the smallest drive.
Since the other drive is still 6TB, the new 16TB drive will only use 6TB for mirroring.
The expansion will happen after replacing the second drive.</p>
</div></div>
<h3 class="heading"><a href="https://blog.marcnuri.com/synology-nas-ds224-hard-drive-upgrade-raid1#step-7-wait-repair" aria-label="step-7-wait-repair permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="step-7-wait-repair"></span>Step 7: Wait for the first repair to complete</h3>
<p>The repair process will take several hours:</p>
<ul>
<li><strong>Status</strong>: The storage pool will show as repairing</li>
<li><strong>Duration</strong>: In my case, about <strong>10.5 hours</strong> for ~5.4TB of data</li>
<li>Monitor progress in <strong>Storage Manager</strong></li>
</ul>
<div class="admonition admonition__warning"><p class="admonition-title"><i class="admonition-title-icon fa-solid fa-exclamation-triangle"></i>Warning</p><div class="admonition-content">
<p>Do not power off the NAS or replace the second drive until the repair is complete.
The storage pool must return to "Healthy" status before proceeding.</p>
</div></div>
<h3 class="heading"><a href="https://blog.marcnuri.com/synology-nas-ds224-hard-drive-upgrade-raid1#step-8-replace-second-drive" aria-label="step-8-replace-second-drive permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="step-8-replace-second-drive"></span>Step 8: Replace the second drive</h3>
<p>Once the first repair is complete and the storage pool shows <strong>Healthy</strong>, it's time to replace the second drive:</p>
<ol>
<li>Repeat <strong>Steps 1-6</strong> for the second drive (locate, deactivate, swap, repair)</li>
<li>During the repair process, you should now see the option to <strong>expand the capacity</strong></li>
<li>Check the box to expand, as both drives will now be 16TB</li>
</ol>
<p>As shown in the screenshot below, this time the repair dialog includes an option to expand the storage pool capacity.
This is because both drives will now be 16TB, allowing the RAID to use the full capacity.</p>
<span class="post-image__pusher "></span><figure class="post-image "><span class="post-image__scrim"></span><a href="/static/ac5e8d153155416325715ee4788d9142/03b5e/repair-and-expand-drive-2.png" class="post-image__link" title="Repair and Expand Storage Pool - Drive 2"><span class="post-image__image-container"><div data-gatsby-image-wrapper="" class="gatsby-image-wrapper gatsby-image-wrapper-constrained post-image__image "><picture><source type="image/webp" data-srcset="/static/ac5e8d153155416325715ee4788d9142/7b968/repair-and-expand-drive-2.webp 205w,/static/ac5e8d153155416325715ee4788d9142/64037/repair-and-expand-drive-2.webp 410w,/static/ac5e8d153155416325715ee4788d9142/3f813/repair-and-expand-drive-2.webp 820w" sizes="(min-width: 820px) 820px, 100vw"><img data-gatsby-image-ssr="" data-main-image="" style="opacity: 1;" sizes="(min-width: 820px) 820px, 100vw" decoding="async" loading="lazy" data-src="/static/ac5e8d153155416325715ee4788d9142/03b5e/repair-and-expand-drive-2.png" data-srcset="/static/ac5e8d153155416325715ee4788d9142/25c95/repair-and-expand-drive-2.png 205w,/static/ac5e8d153155416325715ee4788d9142/fc026/repair-and-expand-drive-2.png 410w,/static/ac5e8d153155416325715ee4788d9142/03b5e/repair-and-expand-drive-2.png 820w" alt="Repair and Expand Storage Pool - Drive 2" src="https://blog.marcnuri.com/static/ac5e8d153155416325715ee4788d9142/03b5e/repair-and-expand-drive-2.png" srcset="https://blog.marcnuri.com/static/ac5e8d153155416325715ee4788d9142/25c95/repair-and-expand-drive-2.png 205w,https://blog.marcnuri.com/static/ac5e8d153155416325715ee4788d9142/fc026/repair-and-expand-drive-2.png 410w,https://blog.marcnuri.com/static/ac5e8d153155416325715ee4788d9142/03b5e/repair-and-expand-drive-2.png 820w"></picture><script type="module">const t="undefined"!=typeof HTMLImageElement&&"loading"in HTMLImageElement.prototype;if(t){const t=document.querySelectorAll("img[data-main-image]");for(let e of t){e.dataset.src&&(e.setAttribute("src",e.dataset.src),e.removeAttribute("data-src")),e.dataset.srcset&&(e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset"));const t=e.parentNode.querySelectorAll("source[data-srcset]");for(let e of t)e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset");e.complete&&(e.style.opacity=1,e.parentNode.parentNode.querySelector("[data-placeholder-image]").style.opacity=0)}}</script></div></span></a></figure>
<h3 class="heading"><a href="https://blog.marcnuri.com/synology-nas-ds224-hard-drive-upgrade-raid1#step-9-wait-second-repair" aria-label="step-9-wait-second-repair permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="step-9-wait-second-repair"></span>Step 9: Wait for the second repair and expansion</h3>
<p>Wait for the second repair and expansion to complete:</p>
<ul>
<li><strong>Repair duration</strong>: About <strong>6-7 hours</strong> for the RAID rebuild</li>
<li><strong>Expansion duration</strong>: About <strong>14-15 hours</strong> for the storage pool expansion</li>
<li><strong>Total duration</strong>: <strong>21.5 hours</strong></li>
<li>Once complete, the storage pool will show <strong>Healthy</strong> with the new 14.5TB capacity</li>
</ul>
<p>Interestingly, the repair phase was faster than the first drive (6-7 hours vs 10.5 hours), but the expansion phase added significant time to the process.</p>
<h2 class="heading"><a href="https://blog.marcnuri.com/synology-nas-ds224-hard-drive-upgrade-raid1#verification" aria-label="verification permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="verification"></span>Verification</h2>
<p>After completing the upgrade, verify that everything is working correctly:</p>
<ul>
<li>✅ Storage pool shows <strong>Healthy</strong></li>
<li>✅ Total capacity reflects the new drive sizes</li>
<li>✅ All shared folders are accessible</li>
<li>✅ Data integrity check (spot-check some files)</li>
</ul>
<p>The screenshot below shows the final storage pool configuration with both 16TB drives healthy and the full capacity available.</p>
<span class="post-image__pusher "></span><figure class="post-image "><span class="post-image__scrim"></span><a href="/static/f1b5f137180bda335787fef4d85c8bee/a5696/resulting-storage-pool.png" class="post-image__link" title="Final Storage Pool Configuration"><span class="post-image__image-container"><div data-gatsby-image-wrapper="" class="gatsby-image-wrapper gatsby-image-wrapper-constrained post-image__image "><picture><source type="image/webp" data-srcset="/static/f1b5f137180bda335787fef4d85c8bee/4e526/resulting-storage-pool.webp 238w,/static/f1b5f137180bda335787fef4d85c8bee/ec7dc/resulting-storage-pool.webp 477w,/static/f1b5f137180bda335787fef4d85c8bee/7dac1/resulting-storage-pool.webp 953w" sizes="(min-width: 953px) 953px, 100vw"><img data-gatsby-image-ssr="" data-main-image="" style="opacity: 1;" sizes="(min-width: 953px) 953px, 100vw" decoding="async" loading="lazy" data-src="/static/f1b5f137180bda335787fef4d85c8bee/a5696/resulting-storage-pool.png" data-srcset="/static/f1b5f137180bda335787fef4d85c8bee/016c9/resulting-storage-pool.png 238w,/static/f1b5f137180bda335787fef4d85c8bee/3d36f/resulting-storage-pool.png 477w,/static/f1b5f137180bda335787fef4d85c8bee/a5696/resulting-storage-pool.png 953w" alt="Final Storage Pool Configuration" src="https://blog.marcnuri.com/static/f1b5f137180bda335787fef4d85c8bee/a5696/resulting-storage-pool.png" srcset="https://blog.marcnuri.com/static/f1b5f137180bda335787fef4d85c8bee/016c9/resulting-storage-pool.png 238w,https://blog.marcnuri.com/static/f1b5f137180bda335787fef4d85c8bee/3d36f/resulting-storage-pool.png 477w,https://blog.marcnuri.com/static/f1b5f137180bda335787fef4d85c8bee/a5696/resulting-storage-pool.png 953w"></picture><script type="module">const t="undefined"!=typeof HTMLImageElement&&"loading"in HTMLImageElement.prototype;if(t){const t=document.querySelectorAll("img[data-main-image]");for(let e of t){e.dataset.src&&(e.setAttribute("src",e.dataset.src),e.removeAttribute("data-src")),e.dataset.srcset&&(e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset"));const t=e.parentNode.querySelectorAll("source[data-srcset]");for(let e of t)e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset");e.complete&&(e.style.opacity=1,e.parentNode.parentNode.querySelector("[data-placeholder-image]").style.opacity=0)}}</script></div></span></a></figure>
<p><strong>Final configuration:</strong></p>
<ul>
<li><strong>New drives</strong>: 2x Seagate IronWolf Pro 16TB (ST16000NT001)</li>
<li><strong>Total raw capacity</strong>: 32TB (2x 16TB)</li>
<li><strong>Usable capacity with RAID 1</strong>: <strong>14.5TB</strong></li>
</ul>
<h2 class="heading"><a href="https://blog.marcnuri.com/synology-nas-ds224-hard-drive-upgrade-raid1#timeline-summary" aria-label="timeline-summary permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="timeline-summary"></span>Timeline summary</h2>
<table><thead><tr><th align="left">Step</th><th align="left">Duration</th></tr></thead><tbody><tr><td align="left">Data scrubbing (before starting)</td><td align="left">9 hours</td></tr><tr><td align="left">First repair</td><td align="left">10.5 hours</td></tr><tr><td align="left">Second repair and expansion</td><td align="left">21.5 hours</td></tr><tr><td align="left"><strong>Total time</strong></td><td align="left"><strong>~41 hours</strong></td></tr></tbody></table>
<h2 class="heading"><a href="https://blog.marcnuri.com/synology-nas-ds224-hard-drive-upgrade-raid1#conclusion" aria-label="conclusion permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="conclusion"></span>Conclusion</h2>
<p>Upgrading the hard drives in my Synology DS224+ was a straightforward process that required minimal hands-on effort.
Despite the total process taking about 41 hours, the actual work on my part was just a few minor operations: deactivate, swap, repair, and repeat.
The NAS handled everything else autonomously, allowing me to <a class="post-link " title="Boosting My Developer Productivity with AI in 2025" href="/boosting-developer-productivity-ai-2025">focus on other tasks</a> while it rebuilt and expanded the storage pool.</p>
<p>This experience reinforced why I chose a Synology NAS in the first place.
The premium cost is justified by the polished software, reliable hardware, and well-documented procedures that save time and reduce the risk of errors.
As a busy person, I value solutions that work reliably without requiring constant attention or troubleshooting.</p>
<p><strong>Key takeaways:</strong></p>
<ul>
<li><strong>Minimal effort</strong>: Just a few clicks and physical drive swaps - the NAS handles the rest</li>
<li><strong>No data loss</strong>: RAID 1 allows replacing drives one at a time while maintaining redundancy</li>
<li><strong>Patience required</strong>: Plan for ~41 hours total (data scrubbing + two repair cycles + expansion)</li>
<li><strong>Nearly 3x capacity</strong>: Upgraded from 5.4TB to 14.5TB usable storage</li>
</ul>
<p>The extra storage headroom should serve me well for the foreseeable future, and when it's time to upgrade again, I know the process will be just as painless.</p>
<h2 class="heading"><a href="https://blog.marcnuri.com/synology-nas-ds224-hard-drive-upgrade-raid1#references" aria-label="references permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="references"></span>References</h2>
<ul>
<li><a href="https://www.synology.com/products/DS224+" rel="noopener" title="Link to https://www.synology.com/products/DS224+" aria-label="Synology DS224+ Specifications" target="_blank">Synology DS224+ Specifications</a></li>
<li><a href="https://www.synology.com/compatibility" rel="noopener" title="Link to https://www.synology.com/compatibility" aria-label="Synology HDD Compatibility List" target="_blank">Synology HDD Compatibility List</a></li>
<li><a href="https://kb.synology.com/en-us/DSM/help/DSM/StorageManager/storage_pool_expand_replace_disk" rel="noopener" title="Link to https://kb.synology.com/en-us/DSM/help/DSM/StorageManager/storage_pool_expand_replace_disk" aria-label="Replace a Drive - Synology Knowledge Center" target="_blank">Replace a Drive - Synology Knowledge Center</a></li>
</ul>
  ]]></content:encoded>
            <category>Operations</category>
            <enclosure url="https://blog.marcnuri.com/static/bcb208420a036d87c3072458443917da/818f3/synology-ds224%2B.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Fabric8 Kubernetes Client 7.5 is now available!]]></title>
            <link>https://blog.marcnuri.com/fabric8-kubernetes-client-7-5</link>
            <guid isPermaLink="false">https://blog.marcnuri.com/fabric8-kubernetes-client-7-5</guid>
            <pubDate>Mon, 12 Jan 2026 08:00:00 GMT</pubDate>
            <description><![CDATA[Fabric8 Kubernetes Client 7.5 is available! Check out the major changes and learn how you can contribute.]]></description>
            <content:encoded><![CDATA[
    <div><a href="https://blog.marcnuri.com/fabric8-kubernetes-client-7-5">Original post</a></div>
    <div class="admonition admonition__note"><p class="admonition-title"><i class="admonition-title-icon fa-solid fa-circle-info"></i>Note</p><div class="admonition-content">A newer version of Fabric8 Kubernetes Client is available, jump to <a class="post-link " title="Fabric8 Kubernetes Client 7.6 is now available!" href="/fabric8-kubernetes-client-7-6">Fabric8 Kubernetes Client 7.6 announcement</a>.</div></div>
<p>On behalf of the <a class="post-link " title="Kubernetes Client for Java: Fabric8 introduction" href="/kubernetes-client-java-fabric8-introduction">Fabric8</a>
team and everyone who has contributed, I'm happy to announce that the Fabric8 Kubernetes Client <code>7.5.2</code> has been
<a href="https://github.com/fabric8io/kubernetes-client/releases/tag/v7.5.2" rel="noopener" title="Link to https://github.com/fabric8io/kubernetes-client/releases/tag/v7.5.2" aria-label="released" target="_blank">released</a> and is now available from
<a href="https://repo1.maven.org/maven2/io/fabric8/kubernetes-client/7.5.2/" rel="noopener" title="Link to https://repo1.maven.org/maven2/io/fabric8/kubernetes-client/7.5.2/" aria-label="Maven Central" target="_blank">Maven Central</a> 🎉.</p>
<p>This marks the fifth minor release of the Fabric8 Kubernetes Client 7, bringing new features, bug fixes, and improvements while keeping the breaking changes minimal.</p>
<p>Thanks to all of you who have contributed with issue reports, pull requests, feedback, and spreading the word with blogs, videos, comments, and so on.
We really appreciate your help, keep it up!</p>
<h2 class="heading"><a href="https://blog.marcnuri.com/fabric8-kubernetes-client-7-5#whats-new" aria-label="whats-new permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="whats-new"></span>What's new?</h2>
<p>Without further ado, let's have a look at the most significant updates:</p>
<ul>
<li><a class="post-link " title="Link to the Kubernetes 1.34 section" href="/fabric8-kubernetes-client-7-5#kubernetes-134">Kubernetes 1.34 support with updated OpenShift 4.20 model</a></li>
<li><a class="post-link " title="Link to the Streaming List section" href="/fabric8-kubernetes-client-7-5#streaming-list">Streaming list support via new Watchable.streamingList method</a></li>
<li><a class="post-link " title="Link to the CRD Improvements section" href="/fabric8-kubernetes-client-7-5#crd-improvements">CRD generator enhancements with SchemaCustomizer annotation</a></li>
<li><a class="post-link " title="Link to the Stability Fixes section" href="/fabric8-kubernetes-client-7-5#stability-fixes">Leader election and WebSocket stability fixes</a></li>
<li>🐛 Many other bug fixes and minor improvements</li>
</ul>
<p>You can find the full changelog for this version in our GitHub <a href="https://github.com/fabric8io/kubernetes-client/releases/tag/v7.5.0" rel="noopener" title="Link to https://github.com/fabric8io/kubernetes-client/releases/tag/v7.5.0" aria-label="release page" target="_blank">release page</a>.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/fabric8-kubernetes-client-7-5#kubernetes-134" aria-label="kubernetes-134 permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="kubernetes-134"></span>Kubernetes 1.34 support with updated OpenShift 4.20 model</h3>
<p>This release adds support for Kubernetes v1.34 (Of Wind &amp; Will) and updates the Fabric8 OpenShift Model as per OpenShift 4.20.
The Gateway API has also been updated from 1.2.1 to 1.4.0, and Istio from 1.27 to 1.28, ensuring you have access to the latest CRDs and resources.</p>
<p>Note that <code>ValidatingAdmissionPolicy</code> and related classes have been removed from <code>admissionregistration.v1beta1</code> as they graduated to GA.
The <code>openshift-model-installer</code> module is now deprecated and will be removed in a future release.</p>
<div class="admonition admonition__note"><p class="admonition-title"><i class="admonition-title-icon fa-solid fa-circle-info"></i>Note</p><div class="admonition-content">
<p>Please note that you can still access newer Kubernetes clusters with <strong>older</strong> versions of the Fabric8 client.</p>
<p>The client provides a GenericKubernetesResources class to interact with resources that are not yet supported by the client.
We do recommend to always use the <strong>latest</strong> version of the client to benefit from the latest features and bug fixes, but it's not mandatory.</p>
</div></div>
<h3 class="heading"><a href="https://blog.marcnuri.com/fabric8-kubernetes-client-7-5#streaming-list" aria-label="streaming-list permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="streaming-list"></span>Streaming list support via new Watchable.streamingList method</h3>
<p>A highly requested feature has been added: streaming lists via the new <code>Watchable.streamingList()</code> method.
This allows you to process large lists of resources incrementally without loading the entire list into memory, which is particularly useful when dealing with namespaces containing thousands of resources.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/fabric8-kubernetes-client-7-5#crd-improvements" aria-label="crd-improvements permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="crd-improvements"></span>CRD generator enhancements with SchemaCustomizer annotation</h3>
<p>The CRD generator continues to evolve with the addition of the <code>@SchemaCustomizer</code> annotation for advanced schema modification.
This new annotation supports <code>@Repeatable</code>, allowing you to apply multiple customizations, and provides better exception messages when things go wrong.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/fabric8-kubernetes-client-7-5#stability-fixes" aria-label="stability-fixes permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="stability-fixes"></span>Leader election and WebSocket stability fixes</h3>
<p>Several important stability fixes have been implemented:</p>
<ul>
<li>Leader election callbacks are now called only once instead of twice, fixing duplicate execution issues</li>
<li>The duration from the current leader record is now used instead of the config duration, ensuring proper leader election behavior</li>
<li>Vert.x WebSockets now properly handle multiple frames</li>
<li>Request configuration is preserved when adapting to OpenShiftClient</li>
</ul>
<h2 class="heading"><a href="https://blog.marcnuri.com/fabric8-kubernetes-client-7-5#using-this-release" aria-label="using-this-release permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="using-this-release"></span>Using this release</h2>
<p>If your project is based on Maven, you just need to add the Fabric8 Kubernetes Client to your Maven dependencies:</p>
<div class="code" style="background:#232323"><pre style="display:block;overflow-x:auto;padding:0.5em;background:#232323;color:#e6e1dc"><code class="language-xml" style="white-space:pre"><span style="color:#e8bf6a">&lt;</span><span style="color:#e8bf6a">dependency</span><span style="color:#e8bf6a">&gt;</span><span>
</span><span>  </span><span style="color:#e8bf6a">&lt;</span><span style="color:#e8bf6a">groupId</span><span style="color:#e8bf6a">&gt;</span><span>io.fabric8</span><span style="color:#e8bf6a">&lt;/</span><span style="color:#e8bf6a">groupId</span><span style="color:#e8bf6a">&gt;</span><span>
</span><span>  </span><span style="color:#e8bf6a">&lt;</span><span style="color:#e8bf6a">artifactId</span><span style="color:#e8bf6a">&gt;</span><span>kubernetes-client</span><span style="color:#e8bf6a">&lt;/</span><span style="color:#e8bf6a">artifactId</span><span style="color:#e8bf6a">&gt;</span><span>
</span><span>  </span><span style="color:#e8bf6a">&lt;</span><span style="color:#e8bf6a">version</span><span style="color:#e8bf6a">&gt;</span><span>7.5.2</span><span style="color:#e8bf6a">&lt;/</span><span style="color:#e8bf6a">version</span><span style="color:#e8bf6a">&gt;</span><span>
</span><span></span><span style="color:#e8bf6a">&lt;/</span><span style="color:#e8bf6a">dependency</span><span style="color:#e8bf6a">&gt;</span></code></pre></div>
<p>If your project is based on Gradle, you just need to add the Fabric8 Kubernetes Client to your Gradle dependencies:</p>
<div class="code" style="background:#232323"><pre style="display:block;overflow-x:auto;padding:0.5em;background:#232323;color:#e6e1dc"><code class="language-groovy" style="white-space:pre"><span>dependencies {
</span><span>  api </span><span style="color:#a5c261">"io.fabric8:kubernetes-client:7.5.2"</span><span>
</span>}</code></pre></div>
<p>Once your project is ready, you can create a new instance of the client to perform operations.
In the following code snippet, I show you how to instantiate the client and retrieve a list of Pods:</p>
<div class="code" style="background:#232323"><pre style="display:block;overflow-x:auto;padding:0.5em;background:#232323;color:#e6e1dc"><code class="language-java" style="white-space:pre"><span style="color:#c26230">try</span><span> (KubernetesClient client = </span><span style="color:#c26230">new</span><span> KubernetesClientBuilder().build()) {
</span>  client.pods().list().getItems().forEach(p -&gt; System.out.println(p.getMetadata().getName()));
<!-- -->}</code></pre></div>
<h2 class="heading"><a href="https://blog.marcnuri.com/fabric8-kubernetes-client-7-5#how-can-you-help" aria-label="how-can-you-help permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="how-can-you-help"></span>How can you help?</h2>
<p>If you're interested in helping out and are a first-time contributor, check out
the <a href="https://github.com/fabric8io/kubernetes-client/labels/good%20first%20issue" rel="noopener" title="Link to https://github.com/fabric8io/kubernetes-client/labels/good%20first%20issue" aria-label="&quot;good first issue&quot;" target="_blank">"good first issue"</a> tag in the issue repository.
We've tagged extremely easy issues so that you can get started contributing to Open Source.</p>
<p>We're also excited to read articles and posts mentioning our project and sharing the user experience.
Giving a star to the project, and spreading the word in general, helps us reach more users and broaden the
feedback. Feedback is the only way to improve.</p>
<p><a href="https://github.com/fabric8io/kubernetes-client" rel="noopener" title="Link to https://github.com/fabric8io/kubernetes-client" aria-label="Project Page" target="_blank">Project Page</a> |
<a href="https://github.com/fabric8io/kubernetes-client/issues" rel="noopener" title="Link to https://github.com/fabric8io/kubernetes-client/issues" aria-label="Issues" target="_blank">Issues</a> |
<a href="https://github.com/fabric8io/kubernetes-client/discussions" rel="noopener" title="Link to https://github.com/fabric8io/kubernetes-client/discussions" aria-label="Discussions" target="_blank">Discussions</a> |
<a href="https://gitter.im/fabric8io/kubernetes-client" rel="noopener" title="Link to https://gitter.im/fabric8io/kubernetes-client" aria-label="Gitter" target="_blank">Gitter</a> |
<a href="https://stackoverflow.com/questions/tagged/fabric8" rel="noopener" title="Link to https://stackoverflow.com/questions/tagged/fabric8" aria-label="Stack Overflow" target="_blank">Stack Overflow</a></p>
<span class="post-image__pusher "></span><figure class="post-image "><span class="post-image__scrim"></span><a href="/static/eb11f2059915fc28b71c76eff27dc487/539a4/fabric8-logo.png" class="post-image__link" title="The logo of Fabric8 Kubernetes Client"><span class="post-image__image-container"><div data-gatsby-image-wrapper="" class="gatsby-image-wrapper gatsby-image-wrapper-constrained post-image__image "><picture><source type="image/webp" data-srcset="/static/eb11f2059915fc28b71c76eff27dc487/a66c2/fabric8-logo.webp 269w,/static/eb11f2059915fc28b71c76eff27dc487/3860d/fabric8-logo.webp 539w,/static/eb11f2059915fc28b71c76eff27dc487/418ed/fabric8-logo.webp 1077w" sizes="(min-width: 1077px) 1077px, 100vw"><img data-gatsby-image-ssr="" data-main-image="" style="opacity: 1;" sizes="(min-width: 1077px) 1077px, 100vw" decoding="async" loading="lazy" data-src="/static/eb11f2059915fc28b71c76eff27dc487/539a4/fabric8-logo.png" data-srcset="/static/eb11f2059915fc28b71c76eff27dc487/53e69/fabric8-logo.png 269w,/static/eb11f2059915fc28b71c76eff27dc487/f0c2e/fabric8-logo.png 539w,/static/eb11f2059915fc28b71c76eff27dc487/539a4/fabric8-logo.png 1077w" alt="The logo of Fabric8 Kubernetes Client" src="https://blog.marcnuri.com/static/eb11f2059915fc28b71c76eff27dc487/539a4/fabric8-logo.png" srcset="https://blog.marcnuri.com/static/eb11f2059915fc28b71c76eff27dc487/53e69/fabric8-logo.png 269w,https://blog.marcnuri.com/static/eb11f2059915fc28b71c76eff27dc487/f0c2e/fabric8-logo.png 539w,https://blog.marcnuri.com/static/eb11f2059915fc28b71c76eff27dc487/539a4/fabric8-logo.png 1077w"></picture><script type="module">const t="undefined"!=typeof HTMLImageElement&&"loading"in HTMLImageElement.prototype;if(t){const t=document.querySelectorAll("img[data-main-image]");for(let e of t){e.dataset.src&&(e.setAttribute("src",e.dataset.src),e.removeAttribute("data-src")),e.dataset.srcset&&(e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset"));const t=e.parentNode.querySelectorAll("source[data-srcset]");for(let e of t)e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset");e.complete&&(e.style.opacity=1,e.parentNode.parentNode.querySelector("[data-placeholder-image]").style.opacity=0)}}</script></div></span></a></figure>
  ]]></content:encoded>
            <category>Cloud Native</category>
            <enclosure url="https://blog.marcnuri.com/static/b9dbe50d753954e05cf28f5e3aa1c88a/baaed/fabric8.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Boosting My Developer Productivity with AI in 2025]]></title>
            <link>https://blog.marcnuri.com/boosting-developer-productivity-ai-2025</link>
            <guid isPermaLink="false">https://blog.marcnuri.com/boosting-developer-productivity-ai-2025</guid>
            <pubDate>Mon, 22 Dec 2025 06:00:00 GMT</pubDate>
            <description><![CDATA[How I restructured my software development workflow with asynchronous AI coding agents to dramatically boost developer productivity in 2025.]]></description>
            <content:encoded><![CDATA[
    <div><a href="https://blog.marcnuri.com/boosting-developer-productivity-ai-2025">Original post</a></div>
    <h2 class="heading"><a href="https://blog.marcnuri.com/boosting-developer-productivity-ai-2025#introduction" aria-label="introduction permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="introduction"></span>Introduction</h2>
<p>I don't usually share my <a href="https://github.com/manusa" rel="noopener" title="Link to https://github.com/manusa" aria-label="GitHub contributions graph" target="_blank">GitHub contributions graph</a>.
If anything, it's evidence of an unhealthy obsession with software development rather than something to brag about.
This time I'm making an exception because it tells a short, useful story.</p>
<span class="post-image__pusher "></span><figure class="post-image "><span class="post-image__scrim"></span><a href="/static/9a4f7b164ae59a6ab6664022523c73e9/0ef02/github-manusa-contributions.png" class="post-image__link" title="An image of Marc Nuri's GitHub Contribution Graph"><span class="post-image__image-container"><div data-gatsby-image-wrapper="" class="gatsby-image-wrapper gatsby-image-wrapper-constrained post-image__image "><picture><source type="image/webp" data-srcset="/static/9a4f7b164ae59a6ab6664022523c73e9/c8cb5/github-manusa-contributions.webp 183w,/static/9a4f7b164ae59a6ab6664022523c73e9/19fe0/github-manusa-contributions.webp 367w,/static/9a4f7b164ae59a6ab6664022523c73e9/24fc7/github-manusa-contributions.webp 733w" sizes="(min-width: 733px) 733px, 100vw"><img data-gatsby-image-ssr="" data-main-image="" style="opacity: 1;" sizes="(min-width: 733px) 733px, 100vw" decoding="async" loading="lazy" data-src="/static/9a4f7b164ae59a6ab6664022523c73e9/0ef02/github-manusa-contributions.png" data-srcset="/static/9a4f7b164ae59a6ab6664022523c73e9/6dcb9/github-manusa-contributions.png 183w,/static/9a4f7b164ae59a6ab6664022523c73e9/b5f0d/github-manusa-contributions.png 367w,/static/9a4f7b164ae59a6ab6664022523c73e9/0ef02/github-manusa-contributions.png 733w" alt="An image of Marc Nuri's GitHub Contribution Graph" src="https://blog.marcnuri.com/static/9a4f7b164ae59a6ab6664022523c73e9/0ef02/github-manusa-contributions.png" srcset="https://blog.marcnuri.com/static/9a4f7b164ae59a6ab6664022523c73e9/6dcb9/github-manusa-contributions.png 183w,https://blog.marcnuri.com/static/9a4f7b164ae59a6ab6664022523c73e9/b5f0d/github-manusa-contributions.png 367w,https://blog.marcnuri.com/static/9a4f7b164ae59a6ab6664022523c73e9/0ef02/github-manusa-contributions.png 733w"></picture><script type="module">const t="undefined"!=typeof HTMLImageElement&&"loading"in HTMLImageElement.prototype;if(t){const t=document.querySelectorAll("img[data-main-image]");for(let e of t){e.dataset.src&&(e.setAttribute("src",e.dataset.src),e.removeAttribute("data-src")),e.dataset.srcset&&(e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset"));const t=e.parentNode.querySelectorAll("source[data-srcset]");for(let e of t)e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset");e.complete&&(e.style.opacity=1,e.parentNode.parentNode.querySelector("[data-placeholder-image]").style.opacity=0)}}</script></div></span></a></figure>
<p>Look at the last three months.
After my summer break I restructured my developer workflows and went all-in on <a class="category-link " title="Artificial Intelligence: Everything related to artificial intelligence (AI) and machine learning (ML)" aria-label="AI tooling" href="/category/ai">AI tooling</a>.
The results speak for themselves, I went from 10-15 contributions per day to over 25 contributions per day on average.</p>
<p>I've used AI-assisted development ever since <a class="tag-link " title="GitHub" aria-label="GitHub" href="/tag/github">GitHub</a> Copilot arrived.
For my personal projects, at least, I've had the luxury of exploring these tools without restrictions.</p>
<p>But this year, I pushed harder.
I integrated AI into every possible workflow and measured the outcomes.
The productivity gains have been remarkable.</p>
<p>Throughout this post, I'll be using examples from my personal projects like <a href="https://github.com/manusa/electronim" rel="noopener" title="Link to https://github.com/manusa/electronim" aria-label="ElectronIM" target="_blank">ElectronIM</a>, <a href="https://github.com/manusa/yakd" rel="noopener" title="Link to https://github.com/manusa/yakd" aria-label="YAKD" target="_blank">YAKD</a>, and <a href="https://github.com/manusa/helm-java" rel="noopener" title="Link to https://github.com/manusa/helm-java" aria-label="helm-java" target="_blank">helm-java</a> to illustrate these concepts.
These projects aren't bound by corporate policies, allowing me to experiment freely with tools like GitHub Copilot and others.
That said, the practices I describe here apply wherever your organization's policies permit.</p>
<h2 class="heading"><a href="https://blog.marcnuri.com/boosting-developer-productivity-ai-2025#how-i-measured-impact" aria-label="how-i-measured-impact permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="how-i-measured-impact"></span>How I measured impact</h2>
<p>Before anything else: I tried to be concrete about "productivity".
My measurements are approximate and meant to show direction rather than provide definitive benchmarks.</p>
<ul>
<li>Commit and Pull Request (PR) velocity (commits/day, PRs opened and merged).</li>
<li>Time-to-merge for small, routine changes.</li>
<li>Number of automated refactors completed without manual edits.</li>
<li>Qualitative: how often I could review and merge from my phone or while away from the keyboard (AFK).</li>
</ul>
<p>Thinking of trying this yourself? Run a small experiment: measure your current output for a month, then adopt AI tooling and quantify your gains.</p>
<p>These numbers reflect my experience as a senior maintainer working on mature projects with strong test coverage.
I wouldn't expect the same gains in early-stage or poorly structured codebases, and that distinction matters.</p>
<h2 class="heading"><a href="https://blog.marcnuri.com/boosting-developer-productivity-ai-2025#the-ai-tooling-landscape" aria-label="the-ai-tooling-landscape permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="the-ai-tooling-landscape"></span>The AI Tooling Landscape</h2>
<p>Not all AI developer tools are created equal.
After months of experimentation, I've found it useful to categorize them by how they fit into the development workflow:</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/boosting-developer-productivity-ai-2025#autocomplete-the-old-familiar" aria-label="autocomplete-the-old-familiar permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="autocomplete-the-old-familiar"></span>Autocomplete: The Old Familiar</h3>
<p>This is where most developers started their AI journey.
Tools like <a href="https://github.com/copilot" rel="noopener" title="Link to https://github.com/copilot" aria-label="GitHub Copilot's" target="_blank">GitHub Copilot's</a> inline suggestions, IntelliJ's AI assistant, or Cursor's tab completion provide real-time code completion as you type.</p>
<p>I've been using autocomplete features for a couple of years now.
It's convenient, saves keystrokes, and occasionally suggests something clever.
Most importantly, it rarely changes the structure of your work.
But here's the uncomfortable truth: <strong>on its own, it doesn't improve productivity by much</strong>.</p>
<p>Autocomplete is still <strong>synchronous work</strong>.
You're still the one driving, line by line, waiting for suggestions.
The cognitive load remains squarely on your shoulders, for bigger tasks, gains are modest.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/boosting-developer-productivity-ai-2025#ai-enhanced-ides" aria-label="ai-enhanced-ides permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="ai-enhanced-ides"></span>AI-Enhanced IDEs</h3>
<p>Tools like <a href="https://cursor.com" rel="noopener" title="Cursor AI IDE" aria-label="Cursor" target="_blank">Cursor</a> take things a step further by integrating AI more deeply into the IDE experience.
Instead of just completing lines, they can refactor code, answer questions about your codebase, and generate entire functions.</p>
<p>This is a step up from autocomplete, but it still suffers from a fundamental limitation: <strong>it remains synchronous</strong>.
You ask, you wait, you review.
No matter how good the model is, the bottleneck is still you.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/boosting-developer-productivity-ai-2025#chat-based-interfaces" aria-label="chat-based-interfaces permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="chat-based-interfaces"></span>Chat-Based Interfaces</h3>
<p>Chat UIs like <a href="https://chatgpt.com" rel="noopener" title="Simple AI Chat interface" aria-label="ChatGPT" target="_blank">ChatGPT</a>, Claude, or Gemini excel at brainstorming, exploring approaches, and researching solutions.
They are excellent for getting unstuck or exploring solutions before committing to code.</p>
<p>Their drawback is integration friction. Without tight project context, copy/paste and re-contextualization slow things down.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/boosting-developer-productivity-ai-2025#cli-agents" aria-label="cli-agents permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="cli-agents"></span>Command Line Interface (CLI) Agents</h3>
<div class="admonition admonition__note"><p class="admonition-title"><i class="admonition-title-icon fa-solid fa-circle-info"></i>Note</p><div class="admonition-content">
<p>I've since built a <a class="post-link " title="AI Coding Agent Dashboard: Orchestrating Claude Code Across Devices" href="/ai-coding-agent-dashboard">dashboard to orchestrate and monitor these parallel coding agent sessions</a> across multiple devices and projects. Check it out for a practical look at the orchestrator workflow in action.</p>
</div></div>
<p>This is where things get interesting.
CLI-first agents such as <a href="https://github.com/anthropics/claude-code" rel="noopener" title="Link to https://github.com/anthropics/claude-code" aria-label="Claude Code" target="_blank">Claude Code</a>, <a href="https://github.com/google-gemini/gemini-cli" rel="noopener" title="Link to https://github.com/google-gemini/gemini-cli" aria-label="Gemini CLI" target="_blank">Gemini CLI</a>, or <a class="post-link " title="Introducing Goose, the on-machine AI agent" href="/goose-on-machine-ai-agent-cli-introduction">Goose</a> change the game by operating within your project context and invoking multi-step tasks from the terminal, often leveraging <a class="post-link " title="Introduction to the Model Context Protocol (MCP): The Future of AI Integration" href="/model-context-protocol-mcp-introduction">Model Context Protocol (MCP)</a> for tool integration.</p>
<p>CLI agents can:</p>
<ul>
<li>Read and reason about the codebase.</li>
<li>Make changes across multiple files.</li>
<li>Run tests and iterate on failures.</li>
<li>Commit changes with meaningful messages.</li>
</ul>
<p>The key difference is that CLI agents can work <strong>semi-autonomously</strong>.
You give them a task, they execute, you review.
This opens the door to parallelism.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/boosting-developer-productivity-ai-2025#github-issues-and-pull-requests" aria-label="github-issues-and-pull-requests permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="github-issues-and-pull-requests"></span>GitHub Issues and Pull Requests</h3>
<p>This is probably the most underrated category.
AI-powered issue-to-PR workflows, like those enabled by <a href="https://github.com/features/copilot/agents" rel="noopener" title="Link to https://github.com/features/copilot/agents" aria-label="GitHub Copilot Coding Agent" target="_blank">GitHub Copilot Coding Agent</a> or similar tools, let you describe work in prose and receive a ready-to-review pull request.</p>
<p>This workflow resonates deeply with me as a <a class="tag-link " title="RedHattiversary" aria-label="professional open source maintainer" href="/tag/redhattiversary">professional open source maintainer</a>.
It mirrors the asynchronous collaboration model I've used for years: someone opens an issue, proposes changes in a PR, we iterate through comments, and eventually merge.</p>
<p>The critical difference?
<strong>It's completely asynchronous</strong>.
You don't need to be at your computer.
I've literally reviewed and approved AI-generated PRs from my phone.</p>
<p>That said, this workflow has rough edges.
When the model misses the point, iterations become frustrating.
You end up writing correction after correction, wishing you could just edit that one line yourself, but you're locked into the async loop.
I see this as the future of async development, but current implementations need refinement.</p>
<p>When it does work, though, it's remarkable.
For example, I <a href="https://github.com/manusa/electronim/issues/626" rel="noopener" title="Link to https://github.com/manusa/electronim/issues/626" aria-label="described a Task Manager feature" target="_blank">described a Task Manager feature</a> for ElectronIM with clear acceptance criteria and a UI reference, assigned it to Copilot, and reviewed the resulting work during spare moments.
A complete feature, including tests, and everything else was implemented without me writing a single line of code.
All it took was a well-written issue and a well-architected project with good test coverage for the AI agent to work effectively.
Which brings me to the next point, the project factor.</p>
<h2 class="heading"><a href="https://blog.marcnuri.com/boosting-developer-productivity-ai-2025#the-project-factor" aria-label="the-project-factor permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="the-project-factor"></span>The Project Factor</h2>
<p>Here's something I've observed that doesn't get discussed enough: <strong>the state of your project is the single biggest factor in how effective AI tooling will be</strong>.</p>
<p>A well-structured project with:</p>
<ul>
<li>Comprehensive test coverage.</li>
<li>Clear, consistent coding patterns.</li>
<li><a class="post-link " title="Black Box vs White Box Testing: When to Use Each Approach" href="/blackbox-whitebox-testing-comparison">Black-box tests</a> that verify behavior through public interfaces, not implementation, so AI can safely refactor internals without breaking the contract.</li>
<li>Good documentation and clear architecture.</li>
</ul>
<p>...will yield far better results from AI tools than a messy codebase with no tests and inconsistent patterns.</p>
<p>This makes sense when you think about it.
AI tools learn from context.
If your context is chaos, expect chaotic results.
If your context demonstrates clear patterns, the AI will follow them.</p>
<p>This has been one of my most important realizations.
Investing in <a class="category-link " title="Quality Engineering: Everything related to quality engineering, software testing, and test automation" aria-label="code quality" href="/category/quality-engineering">code quality</a> and solid tests isn't just about maintainability anymore.
It's about making your project <strong>AI-ready</strong>.</p>
<h2 class="heading"><a href="https://blog.marcnuri.com/boosting-developer-productivity-ai-2025#the-productivity-multiplier" aria-label="the-productivity-multiplier permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="the-productivity-multiplier"></span>The Productivity Multiplier: Asynchronous and Parallel Development</h2>
<p>Here's the real secret to the productivity gains I've experienced.
It's not about any single tool.
It's about <strong>parallelism</strong>.</p>
<p><span><svg aria-roledescription="gantt" role="graphics-document document" style="max-width: 1000px; background-color: transparent;" viewBox="0 0 1000 196" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="100%" id="mermaid-45"><style>#mermaid-45{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-45 .error-icon{fill:#552222;}#mermaid-45 .error-text{fill:#552222;stroke:#552222;}#mermaid-45 .edge-thickness-normal{stroke-width:1px;}#mermaid-45 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-45 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-45 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-45 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-45 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-45 .marker{fill:#333333;stroke:#333333;}#mermaid-45 .marker.cross{stroke:#333333;}#mermaid-45 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-45 p{margin:0;}#mermaid-45 .mermaid-main-font{font-family:var(--mermaid-font-family, "trebuchet ms", verdana, arial, sans-serif);}#mermaid-45 .exclude-range{fill:#eeeeee;}#mermaid-45 .section{stroke:none;opacity:0.2;}#mermaid-45 .section0{fill:rgba(102, 102, 255, 0.49);}#mermaid-45 .section2{fill:#fff400;}#mermaid-45 .section1,#mermaid-45 .section3{fill:white;opacity:0.2;}#mermaid-45 .sectionTitle0{fill:#333;}#mermaid-45 .sectionTitle1{fill:#333;}#mermaid-45 .sectionTitle2{fill:#333;}#mermaid-45 .sectionTitle3{fill:#333;}#mermaid-45 .sectionTitle{text-anchor:start;font-family:var(--mermaid-font-family, "trebuchet ms", verdana, arial, sans-serif);}#mermaid-45 .grid .tick{stroke:lightgrey;opacity:0.8;shape-rendering:crispEdges;}#mermaid-45 .grid .tick text{font-family:"trebuchet ms",verdana,arial,sans-serif;fill:#333;}#mermaid-45 .grid path{stroke-width:0;}#mermaid-45 .today{fill:none;stroke:red;stroke-width:2px;}#mermaid-45 .task{stroke-width:2;}#mermaid-45 .taskText{text-anchor:middle;font-family:var(--mermaid-font-family, "trebuchet ms", verdana, arial, sans-serif);}#mermaid-45 .taskTextOutsideRight{fill:black;text-anchor:start;font-family:var(--mermaid-font-family, "trebuchet ms", verdana, arial, sans-serif);}#mermaid-45 .taskTextOutsideLeft{fill:black;text-anchor:end;}#mermaid-45 .task.clickable{cursor:pointer;}#mermaid-45 .taskText.clickable{cursor:pointer;fill:#003163!important;font-weight:bold;}#mermaid-45 .taskTextOutsideLeft.clickable{cursor:pointer;fill:#003163!important;font-weight:bold;}#mermaid-45 .taskTextOutsideRight.clickable{cursor:pointer;fill:#003163!important;font-weight:bold;}#mermaid-45 .taskText0,#mermaid-45 .taskText1,#mermaid-45 .taskText2,#mermaid-45 .taskText3{fill:white;}#mermaid-45 .task0,#mermaid-45 .task1,#mermaid-45 .task2,#mermaid-45 .task3{fill:#8a90dd;stroke:#534fbc;}#mermaid-45 .taskTextOutside0,#mermaid-45 .taskTextOutside2{fill:black;}#mermaid-45 .taskTextOutside1,#mermaid-45 .taskTextOutside3{fill:black;}#mermaid-45 .active0,#mermaid-45 .active1,#mermaid-45 .active2,#mermaid-45 .active3{fill:#bfc7ff;stroke:#534fbc;}#mermaid-45 .activeText0,#mermaid-45 .activeText1,#mermaid-45 .activeText2,#mermaid-45 .activeText3{fill:black!important;}#mermaid-45 .done0,#mermaid-45 .done1,#mermaid-45 .done2,#mermaid-45 .done3{stroke:grey;fill:lightgrey;stroke-width:2;}#mermaid-45 .doneText0,#mermaid-45 .doneText1,#mermaid-45 .doneText2,#mermaid-45 .doneText3{fill:black!important;}#mermaid-45 .crit0,#mermaid-45 .crit1,#mermaid-45 .crit2,#mermaid-45 .crit3{stroke:#ff8888;fill:red;stroke-width:2;}#mermaid-45 .activeCrit0,#mermaid-45 .activeCrit1,#mermaid-45 .activeCrit2,#mermaid-45 .activeCrit3{stroke:#ff8888;fill:#bfc7ff;stroke-width:2;}#mermaid-45 .doneCrit0,#mermaid-45 .doneCrit1,#mermaid-45 .doneCrit2,#mermaid-45 .doneCrit3{stroke:#ff8888;fill:lightgrey;stroke-width:2;cursor:pointer;shape-rendering:crispEdges;}#mermaid-45 .milestone{transform:rotate(45deg) scale(0.8,0.8);}#mermaid-45 .milestoneText{font-style:italic;}#mermaid-45 .doneCritText0,#mermaid-45 .doneCritText1,#mermaid-45 .doneCritText2,#mermaid-45 .doneCritText3{fill:black!important;}#mermaid-45 .activeCritText0,#mermaid-45 .activeCritText1,#mermaid-45 .activeCritText2,#mermaid-45 .activeCritText3{fill:black!important;}#mermaid-45 .titleText{text-anchor:middle;font-size:18px;fill:#333;font-family:var(--mermaid-font-family, "trebuchet ms", verdana, arial, sans-serif);}#mermaid-45 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}
  /* blog.marcnuri.com - Custom mermaid overrides */
  /* Make arrows darker for better visibility on light backgrounds */
  .edgePath path, .flowchart-link { stroke: #666 !important; stroke-width: 1.5px !important; }
  .marker path { fill: #666 !important; stroke: #666 !important; }
  .arrowMarkerPath { fill: #666 !important; }
</style><g></g><g text-anchor="middle" font-family="sans-serif" font-size="10" fill="none" transform="translate(75, 146)" class="grid"><path d="M0.5,-111V0.5H850.5V-111" stroke="currentColor" class="domain"></path><g transform="translate(0.5,0)" opacity="1" class="tick"><line y2="-111" stroke="currentColor"></line><text style="text-anchor: middle;" font-size="10" stroke="none" dy="1em" y="3" fill="#000"> 01</text></g><g transform="translate(106.5,0)" opacity="1" class="tick"><line y2="-111" stroke="currentColor"></line><text style="text-anchor: middle;" font-size="10" stroke="none" dy="1em" y="3" fill="#000"> 03</text></g><g transform="translate(213.5,0)" opacity="1" class="tick"><line y2="-111" stroke="currentColor"></line><text style="text-anchor: middle;" font-size="10" stroke="none" dy="1em" y="3" fill="#000"> 05</text></g><g transform="translate(319.5,0)" opacity="1" class="tick"><line y2="-111" stroke="currentColor"></line><text style="text-anchor: middle;" font-size="10" stroke="none" dy="1em" y="3" fill="#000"> 07</text></g><g transform="translate(425.5,0)" opacity="1" class="tick"><line y2="-111" stroke="currentColor"></line><text style="text-anchor: middle;" font-size="10" stroke="none" dy="1em" y="3" fill="#000"> 09</text></g><g transform="translate(531.5,0)" opacity="1" class="tick"><line y2="-111" stroke="currentColor"></line><text style="text-anchor: middle;" font-size="10" stroke="none" dy="1em" y="3" fill="#000"> 11</text></g><g transform="translate(638.5,0)" opacity="1" class="tick"><line y2="-111" stroke="currentColor"></line><text style="text-anchor: middle;" font-size="10" stroke="none" dy="1em" y="3" fill="#000"> 13</text></g><g transform="translate(744.5,0)" opacity="1" class="tick"><line y2="-111" stroke="currentColor"></line><text style="text-anchor: middle;" font-size="10" stroke="none" dy="1em" y="3" fill="#000"> 15</text></g><g transform="translate(850.5,0)" opacity="1" class="tick"><line y2="-111" stroke="currentColor"></line><text style="text-anchor: middle;" font-size="10" stroke="none" dy="1em" y="3" fill="#000"> 17</text></g></g><g><rect class="section section0" height="24" width="962.5" y="48" x="0"></rect><rect class="section section0" height="24" width="962.5" y="72" x="0"></rect><rect class="section section0" height="24" width="962.5" y="96" x="0"></rect><rect class="section section0" height="24" width="962.5" y="120" x="0"></rect></g><g><rect class="task task0" transform-origin="181.5px 60px" height="20" width="213" y="50" x="75" ry="3" rx="3" id="t1"></rect><rect class="task task0" transform-origin="394px 84px" height="20" width="212" y="74" x="288" ry="3" rx="3" id="t2"></rect><rect class="task task0" transform-origin="606.5px 108px" height="20" width="213" y="98" x="500" ry="3" rx="3" id="t3"></rect><rect class="task task0" transform-origin="819px 132px" height="20" width="212" y="122" x="713" ry="3" rx="3" id="t4"></rect><text class="taskText taskText0  width-32.00797653198242" y="63.5" x="181.5" font-size="11" id="t1-text">Task 1 </text><text class="taskText taskText0  width-32.00797653198242" y="87.5" x="394" font-size="11" id="t2-text">Task 2 </text><text class="taskText taskText0  width-32.00797653198242" y="111.5" x="606.5" font-size="11" id="t3-text">Task 3 </text><text class="taskText taskText0  width-32.00801467895508" y="135.5" x="819" font-size="11" id="t4-text">Task 4 </text></g><g><text class="sectionTitle sectionTitle0" font-size="11" y="98" x="10" dy="0em"><tspan x="10" alignment-baseline="central">Tasks</tspan></text></g><g class="today"><line class="today" y2="171" y1="25" x2="22896" x1="22896"></line></g><text class="titleText" y="25" x="500">Traditional Development</text></svg></span></p>
<p>In traditional development, you work on tasks sequentially.
One after another.
Even with autocomplete helping you type faster, you're still limited by your own processing capacity.</p>
<p><span><svg aria-roledescription="gantt" role="graphics-document document" style="max-width: 1000px; background-color: transparent;" viewBox="0 0 1000 556" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="100%" id="mermaid-47"><style>#mermaid-47{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-47 .error-icon{fill:#552222;}#mermaid-47 .error-text{fill:#552222;stroke:#552222;}#mermaid-47 .edge-thickness-normal{stroke-width:1px;}#mermaid-47 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-47 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-47 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-47 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-47 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-47 .marker{fill:#333333;stroke:#333333;}#mermaid-47 .marker.cross{stroke:#333333;}#mermaid-47 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-47 p{margin:0;}#mermaid-47 .mermaid-main-font{font-family:var(--mermaid-font-family, "trebuchet ms", verdana, arial, sans-serif);}#mermaid-47 .exclude-range{fill:#eeeeee;}#mermaid-47 .section{stroke:none;opacity:0.2;}#mermaid-47 .section0{fill:rgba(102, 102, 255, 0.49);}#mermaid-47 .section2{fill:#fff400;}#mermaid-47 .section1,#mermaid-47 .section3{fill:white;opacity:0.2;}#mermaid-47 .sectionTitle0{fill:#333;}#mermaid-47 .sectionTitle1{fill:#333;}#mermaid-47 .sectionTitle2{fill:#333;}#mermaid-47 .sectionTitle3{fill:#333;}#mermaid-47 .sectionTitle{text-anchor:start;font-family:var(--mermaid-font-family, "trebuchet ms", verdana, arial, sans-serif);}#mermaid-47 .grid .tick{stroke:lightgrey;opacity:0.8;shape-rendering:crispEdges;}#mermaid-47 .grid .tick text{font-family:"trebuchet ms",verdana,arial,sans-serif;fill:#333;}#mermaid-47 .grid path{stroke-width:0;}#mermaid-47 .today{fill:none;stroke:red;stroke-width:2px;}#mermaid-47 .task{stroke-width:2;}#mermaid-47 .taskText{text-anchor:middle;font-family:var(--mermaid-font-family, "trebuchet ms", verdana, arial, sans-serif);}#mermaid-47 .taskTextOutsideRight{fill:black;text-anchor:start;font-family:var(--mermaid-font-family, "trebuchet ms", verdana, arial, sans-serif);}#mermaid-47 .taskTextOutsideLeft{fill:black;text-anchor:end;}#mermaid-47 .task.clickable{cursor:pointer;}#mermaid-47 .taskText.clickable{cursor:pointer;fill:#003163!important;font-weight:bold;}#mermaid-47 .taskTextOutsideLeft.clickable{cursor:pointer;fill:#003163!important;font-weight:bold;}#mermaid-47 .taskTextOutsideRight.clickable{cursor:pointer;fill:#003163!important;font-weight:bold;}#mermaid-47 .taskText0,#mermaid-47 .taskText1,#mermaid-47 .taskText2,#mermaid-47 .taskText3{fill:white;}#mermaid-47 .task0,#mermaid-47 .task1,#mermaid-47 .task2,#mermaid-47 .task3{fill:#8a90dd;stroke:#534fbc;}#mermaid-47 .taskTextOutside0,#mermaid-47 .taskTextOutside2{fill:black;}#mermaid-47 .taskTextOutside1,#mermaid-47 .taskTextOutside3{fill:black;}#mermaid-47 .active0,#mermaid-47 .active1,#mermaid-47 .active2,#mermaid-47 .active3{fill:#bfc7ff;stroke:#534fbc;}#mermaid-47 .activeText0,#mermaid-47 .activeText1,#mermaid-47 .activeText2,#mermaid-47 .activeText3{fill:black!important;}#mermaid-47 .done0,#mermaid-47 .done1,#mermaid-47 .done2,#mermaid-47 .done3{stroke:grey;fill:lightgrey;stroke-width:2;}#mermaid-47 .doneText0,#mermaid-47 .doneText1,#mermaid-47 .doneText2,#mermaid-47 .doneText3{fill:black!important;}#mermaid-47 .crit0,#mermaid-47 .crit1,#mermaid-47 .crit2,#mermaid-47 .crit3{stroke:#ff8888;fill:red;stroke-width:2;}#mermaid-47 .activeCrit0,#mermaid-47 .activeCrit1,#mermaid-47 .activeCrit2,#mermaid-47 .activeCrit3{stroke:#ff8888;fill:#bfc7ff;stroke-width:2;}#mermaid-47 .doneCrit0,#mermaid-47 .doneCrit1,#mermaid-47 .doneCrit2,#mermaid-47 .doneCrit3{stroke:#ff8888;fill:lightgrey;stroke-width:2;cursor:pointer;shape-rendering:crispEdges;}#mermaid-47 .milestone{transform:rotate(45deg) scale(0.8,0.8);}#mermaid-47 .milestoneText{font-style:italic;}#mermaid-47 .doneCritText0,#mermaid-47 .doneCritText1,#mermaid-47 .doneCritText2,#mermaid-47 .doneCritText3{fill:black!important;}#mermaid-47 .activeCritText0,#mermaid-47 .activeCritText1,#mermaid-47 .activeCritText2,#mermaid-47 .activeCritText3{fill:black!important;}#mermaid-47 .titleText{text-anchor:middle;font-size:18px;fill:#333;font-family:var(--mermaid-font-family, "trebuchet ms", verdana, arial, sans-serif);}#mermaid-47 .section1{fill:#d97757!important;}#mermaid-47 .section2{fill:#4896e3!important;}#mermaid-47 .section3{fill:#999!important;}#mermaid-47 .section4{fill:#99f!important;}#mermaid-47 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}
  /* blog.marcnuri.com - Custom mermaid overrides */
  /* Make arrows darker for better visibility on light backgrounds */
  .edgePath path, .flowchart-link { stroke: #666 !important; stroke-width: 1.5px !important; }
  .marker path { fill: #666 !important; stroke: #666 !important; }
  .arrowMarkerPath { fill: #666 !important; }
</style><g></g><g text-anchor="middle" font-family="sans-serif" font-size="10" fill="none" transform="translate(75, 506)" class="grid"><path d="M0.5,-471V0.5H850.5V-471" stroke="currentColor" class="domain"></path><g transform="translate(0.5,0)" opacity="1" class="tick"><line y2="-471" stroke="currentColor"></line><text style="text-anchor: middle;" font-size="10" stroke="none" dy="1em" y="3" fill="#000"> 01</text></g><g transform="translate(79.5,0)" opacity="1" class="tick"><line y2="-471" stroke="currentColor"></line><text style="text-anchor: middle;" font-size="10" stroke="none" dy="1em" y="3" fill="#000"> 01</text></g><g transform="translate(158.5,0)" opacity="1" class="tick"><line y2="-471" stroke="currentColor"></line><text style="text-anchor: middle;" font-size="10" stroke="none" dy="1em" y="3" fill="#000"> 02</text></g><g transform="translate(237.5,0)" opacity="1" class="tick"><line y2="-471" stroke="currentColor"></line><text style="text-anchor: middle;" font-size="10" stroke="none" dy="1em" y="3" fill="#000"> 02</text></g><g transform="translate(316.5,0)" opacity="1" class="tick"><line y2="-471" stroke="currentColor"></line><text style="text-anchor: middle;" font-size="10" stroke="none" dy="1em" y="3" fill="#000"> 03</text></g><g transform="translate(395.5,0)" opacity="1" class="tick"><line y2="-471" stroke="currentColor"></line><text style="text-anchor: middle;" font-size="10" stroke="none" dy="1em" y="3" fill="#000"> 03</text></g><g transform="translate(474.5,0)" opacity="1" class="tick"><line y2="-471" stroke="currentColor"></line><text style="text-anchor: middle;" font-size="10" stroke="none" dy="1em" y="3" fill="#000"> 04</text></g><g transform="translate(553.5,0)" opacity="1" class="tick"><line y2="-471" stroke="currentColor"></line><text style="text-anchor: middle;" font-size="10" stroke="none" dy="1em" y="3" fill="#000"> 04</text></g><g transform="translate(633.5,0)" opacity="1" class="tick"><line y2="-471" stroke="currentColor"></line><text style="text-anchor: middle;" font-size="10" stroke="none" dy="1em" y="3" fill="#000"> 05</text></g><g transform="translate(712.5,0)" opacity="1" class="tick"><line y2="-471" stroke="currentColor"></line><text style="text-anchor: middle;" font-size="10" stroke="none" dy="1em" y="3" fill="#000"> 05</text></g><g transform="translate(791.5,0)" opacity="1" class="tick"><line y2="-471" stroke="currentColor"></line><text style="text-anchor: middle;" font-size="10" stroke="none" dy="1em" y="3" fill="#000"> 06</text></g></g><g><rect class="section section0" height="24" width="962.5" y="48" x="0"></rect><rect class="section section0" height="24" width="962.5" y="72" x="0"></rect><rect class="section section1" height="24" width="962.5" y="312" x="0"></rect><rect class="section section0" height="24" width="962.5" y="96" x="0"></rect><rect class="section section2" height="24" width="962.5" y="384" x="0"></rect><rect class="section section0" height="24" width="962.5" y="120" x="0"></rect><rect class="section section3" height="24" width="962.5" y="432" x="0"></rect><rect class="section section0" height="24" width="962.5" y="144" x="0"></rect><rect class="section section1" height="24" width="962.5" y="336" x="0"></rect><rect class="section section0" height="24" width="962.5" y="168" x="0"></rect><rect class="section section0" height="24" width="962.5" y="480" x="0"></rect><rect class="section section0" height="24" width="962.5" y="192" x="0"></rect><rect class="section section2" height="24" width="962.5" y="408" x="0"></rect><rect class="section section0" height="24" width="962.5" y="216" x="0"></rect><rect class="section section1" height="24" width="962.5" y="360" x="0"></rect><rect class="section section0" height="24" width="962.5" y="240" x="0"></rect><rect class="section section3" height="24" width="962.5" y="456" x="0"></rect><rect class="section section0" height="24" width="962.5" y="264" x="0"></rect><rect class="section section0" height="24" width="962.5" y="288" x="0"></rect></g><g><rect class="task task0" transform-origin="114.5px 60px" height="20" width="79" y="50" x="75" ry="3" rx="3" id="i1"></rect><rect class="task task0" transform-origin="193.5px 84px" height="20" width="79" y="74" x="154" ry="3" rx="3" id="i2"></rect><rect class="task task1" transform-origin="233px 324px" height="20" width="158" y="314" x="154" ry="3" rx="3" id="a1"></rect><rect class="task task0" transform-origin="272.5px 108px" height="20" width="79" y="98" x="233" ry="3" rx="3" id="i3"></rect><rect class="task task2" transform-origin="312px 396px" height="20" width="158" y="386" x="233" ry="3" rx="3" id="a2"></rect><rect class="task task0" transform-origin="371.5px 132px" height="20" width="119" y="122" x="312" ry="3" rx="3" id="review1"></rect><rect class="task task3" transform-origin="391px 444px" height="20" width="158" y="434" x="312" ry="3" rx="3" id="a3"></rect><rect class="task task0" transform-origin="470.5px 156px" height="20" width="79" y="146" x="431" ry="3" rx="3" id="i4"></rect><rect class="task task1" transform-origin="450.5px 348px" height="20" width="39" y="338" x="431" ry="3" rx="3" id="a1f1"></rect><rect class="task task0" transform-origin="569px 180px" height="20" width="118" y="170" x="510" ry="3" rx="3" id="review2"></rect><rect class="task task0" transform-origin="668px 492px" height="20" width="316" y="482" x="510" ry="3" rx="3" id="a4"></rect><rect class="task task0" transform-origin="658px 204px" height="20" width="60" y="194" x="628" ry="3" rx="3" id="iterate1"></rect><rect class="task task2" transform-origin="668px 420px" height="20" width="80" y="410" x="628" ry="3" rx="3" id="a2f"></rect><rect class="task task0" transform-origin="727.5px 228px" height="20" width="79" y="218" x="688" ry="3" rx="3" id="review3"></rect><rect class="task task1" transform-origin="707.5px 372px" height="20" width="39" y="362" x="688" ry="3" rx="3" id="a1f2"></rect><rect class="task task0" transform-origin="786.5px 252px" height="20" width="39" y="242" x="767" ry="3" rx="3" id="merge1"></rect><rect class="task task3" transform-origin="806.5px 468px" height="20" width="79" y="458" x="767" ry="3" rx="3" id="a3f"></rect><rect class="task task0" transform-origin="845.5px 276px" height="20" width="79" y="266" x="806" ry="3" rx="3" id="review4"></rect><rect class="task task0" transform-origin="905px 300px" height="20" width="40" y="290" x="885" ry="3" rx="3" id="merge2"></rect><text class="taskText taskText0  width-71.0775375366211" y="63.5" x="114.5" font-size="11" id="i1-text">Create issue 1      </text><text class="taskText taskText0  width-71.0775375366211" y="87.5" x="193.5" font-size="11" id="i2-text">Create issue 2      </text><text class="taskText taskText1  width-32.00734329223633" y="327.5" x="233" font-size="11" id="a1-text">Task 1              </text><text class="taskText taskText0  width-71.0775375366211" y="111.5" x="272.5" font-size="11" id="i3-text">Create issue 3      </text><text class="taskText taskText2  width-32.00734329223633" y="399.5" x="312" font-size="11" id="a2-text">Task 2              </text><text class="taskText taskText0  width-63.74211883544922" y="135.5" x="371.5" font-size="11" id="review1-text">Review PR 1         </text><text class="taskText taskText3  width-32.00734329223633" y="447.5" x="391" font-size="11" id="a3-text">Task 3              </text><text class="taskText taskText0  width-71.0775375366211" y="159.5" x="470.5" font-size="11" id="i4-text">Create issue 4      </text><text class="taskTextOutsideRight taskTextOutside1  width-42.373905181884766" y="351.5" x="475" font-size="11" id="a1f1-text">Fix PR 1            </text><text class="taskText taskText0  width-63.74208068847656" y="183.5" x="569" font-size="11" id="review2-text">Review PR 2         </text><text class="taskText taskText0  width-32.007381439208984" y="495.5" x="668" font-size="11" id="a4-text">Task 4              </text><text class="taskText taskText0  width-58.86787796020508" y="207.5" x="658" font-size="11" id="iterate1-text">Iterate PR 1        </text><text class="taskText taskText2  width-42.37386703491211" y="423.5" x="668" font-size="11" id="a2f-text">Fix PR 2            </text><text class="taskText taskText0  width-63.74211883544922" y="231.5" x="727.5" font-size="11" id="review3-text">Review PR 3         </text><text class="taskTextOutsideRight taskTextOutside1  width-42.37394332885742" y="375.5" x="732" font-size="11" id="a1f2-text">Fix PR 1            </text><text class="taskTextOutsideRight taskTextOutside0  width-58.86250305175781" y="255.5" x="811" font-size="11" id="merge1-text">Merge PR 1          </text><text class="taskText taskText3  width-42.37386703491211" y="471.5" x="806.5" font-size="11" id="a3f-text">Fix PR 3            </text><text class="taskText taskText0  width-63.74211883544922" y="279.5" x="845.5" font-size="11" id="review4-text">Review PR 4         </text><text class="taskTextOutsideLeft taskTextOutside0" y="303.5" x="880" font-size="11" id="merge2-text">Merge PR 2          </text></g><g><text class="sectionTitle sectionTitle0" font-size="11" y="182" x="10" dy="0em"><tspan x="10" alignment-baseline="central">Me</tspan></text><text class="sectionTitle sectionTitle1" font-size="11" y="350" x="10" dy="0em"><tspan x="10" alignment-baseline="central">Agent 1</tspan></text><text class="sectionTitle sectionTitle2" font-size="11" y="410" x="10" dy="0em"><tspan x="10" alignment-baseline="central">Agent 2</tspan></text><text class="sectionTitle sectionTitle3" font-size="11" y="458" x="10" dy="0em"><tspan x="10" alignment-baseline="central">Agent 3</tspan></text><text class="sectionTitle sectionTitle0" font-size="11" y="494" x="10" dy="0em"><tspan x="10" alignment-baseline="central">Agent 4</tspan></text></g><g class="today"><line class="today" y2="531" y1="25" x2="68007" x1="68007"></line></g><text class="titleText" y="25" x="500">AI-Assisted Parallel Development</text></svg></span></p>
<p>With async AI agents, the model flips.
I can have multiple Claude Code instances running on different git worktrees, each tackling a separate task.
Or better yet, using GitHub's web-based workflows, I can have multiple PRs being worked on simultaneously.</p>
<p>My role shifts from <strong>implementer</strong> to <strong>orchestrator</strong>:
I provide direction, review output, and course-correct when needed.</p>
<p>And here's the thing: as a developer, I'm not coding all day.
Meetings, code reviews, emails, one-on-ones; they all interrupt flow.
With async agents, those interruptions become productive gaps.
The agents work while I'm on a call.</p>
<div class="admonition admonition__tip"><p class="admonition-title"><i class="admonition-title-icon fa-regular fa-lightbulb"></i>Tip</p><div class="admonition-content">
<p>To run multiple CLI agents in parallel, you'll need separate working directories.
<a href="https://git-scm.com/docs/git-worktree" rel="noopener" title="Link to https://git-scm.com/docs/git-worktree" aria-label="Git worktrees" target="_blank">Git worktrees</a> are perfect for this, or you can use multiple machines.</p>
</div></div>
<h2 class="heading"><a href="https://blog.marcnuri.com/boosting-developer-productivity-ai-2025#the-uncomfortable-truths" aria-label="the-uncomfortable-truths permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="the-uncomfortable-truths"></span>The Uncomfortable Truths</h2>
<p>It's not all sunshine and rainbows.
Here are some sobering observations:</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/boosting-developer-productivity-ai-2025#burnout-risk" aria-label="burnout-risk permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="burnout-risk"></span>Burnout Risk</h3>
<p>Being able to be productive everywhere and anytime creates the temptation to <strong>never disconnect</strong>.
The cognitive load shifts from implementation to review and orchestration, but it doesn't disappear.
Orchestrating many parallel tasks and constantly switching context is mentally draining.</p>
<p>If you're not careful, this newfound productivity can accelerate burnout rather than prevent it.
Set boundaries.
The work will still be there tomorrow.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/boosting-developer-productivity-ai-2025#the-junior-developer-problem" aria-label="the-junior-developer-problem permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="the-junior-developer-problem"></span>The Junior Developer Problem</h3>
<p>This is uncomfortable to admit, but AI tooling is effectively replacing junior developers in my workflow.
I now have a swarm of AI agents that I can orchestrate like a team of eager interns.
They follow instructions, produce code, and iterate based on feedback, without the learning and growth that human juniors need.</p>
<p>The only limitation is my availability to give instructions and review work.</p>
<p>What does this mean for the next generation of developers?
How do you become a senior developer if you never get to be a junior first?</p>
<p>Organizations need deliberate apprenticeship and mentoring models so people can progress.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/boosting-developer-productivity-ai-2025#coding-is-no-longer-the-job" aria-label="coding-is-no-longer-the-job permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="coding-is-no-longer-the-job"></span>Coding Is No Longer the Job</h3>
<p>I love coding.
I genuinely enjoy the craft of writing elegant solutions.</p>
<p>But increasingly, that's not what I do.
My job has become more managerial: defining tasks, reviewing output, providing feedback, and deciding what to build next.</p>
<span class="post-image__pusher "></span><figure class="post-image "><span class="post-image__scrim"></span><a href="/static/d6beaa60be82907253cabb260a067b0a/204bd/ai-laundry.jpg" class="post-image__link" title="I want AI to do my laundry and dishes so that I can do art and writing. Not for AI to do my art and writing..."><span class="post-image__image-container"><div data-gatsby-image-wrapper="" class="gatsby-image-wrapper gatsby-image-wrapper-constrained post-image__image "><picture><source type="image/webp" data-srcset="/static/d6beaa60be82907253cabb260a067b0a/257df/ai-laundry.webp 270w,/static/d6beaa60be82907253cabb260a067b0a/0f364/ai-laundry.webp 540w,/static/d6beaa60be82907253cabb260a067b0a/e941d/ai-laundry.webp 1080w" sizes="(min-width: 1080px) 1080px, 100vw"><img data-gatsby-image-ssr="" data-main-image="" style="opacity: 1;" sizes="(min-width: 1080px) 1080px, 100vw" decoding="async" loading="lazy" data-src="/static/d6beaa60be82907253cabb260a067b0a/204bd/ai-laundry.jpg" data-srcset="/static/d6beaa60be82907253cabb260a067b0a/6ffa0/ai-laundry.jpg 270w,/static/d6beaa60be82907253cabb260a067b0a/ef519/ai-laundry.jpg 540w,/static/d6beaa60be82907253cabb260a067b0a/204bd/ai-laundry.jpg 1080w" alt="I want AI to do my laundry and dishes so that I can do art and writing. Not for AI to do my art and writing..." src="https://blog.marcnuri.com/static/d6beaa60be82907253cabb260a067b0a/204bd/ai-laundry.jpg" srcset="https://blog.marcnuri.com/static/d6beaa60be82907253cabb260a067b0a/6ffa0/ai-laundry.jpg 270w,https://blog.marcnuri.com/static/d6beaa60be82907253cabb260a067b0a/ef519/ai-laundry.jpg 540w,https://blog.marcnuri.com/static/d6beaa60be82907253cabb260a067b0a/204bd/ai-laundry.jpg 1080w"></picture><script type="module">const t="undefined"!=typeof HTMLImageElement&&"loading"in HTMLImageElement.prototype;if(t){const t=document.querySelectorAll("img[data-main-image]");for(let e of t){e.dataset.src&&(e.setAttribute("src",e.dataset.src),e.removeAttribute("data-src")),e.dataset.srcset&&(e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset"));const t=e.parentNode.querySelectorAll("source[data-srcset]");for(let e of t)e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset");e.complete&&(e.style.opacity=1,e.parentNode.parentNode.querySelector("[data-placeholder-image]").style.opacity=0)}}</script></div></span></a></figure>
<p>This meme captures it perfectly.
The irony isn't lost on me.</p>
<h2 class="heading"><a href="https://blog.marcnuri.com/boosting-developer-productivity-ai-2025#key-takeaways" aria-label="key-takeaways permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="key-takeaways"></span>Key Takeaways</h2>
<p>After months of intensive AI-assisted development, here's what I've learned:</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/boosting-developer-productivity-ai-2025#async-beats-sync" aria-label="async-beats-sync permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="async-beats-sync"></span>Async Beats Sync</h3>
<p>The biggest productivity gains don't come from faster typing or better autocomplete.
They come from <strong>parallelism</strong>.
Web-based, async workflows let you orchestrate multiple AI coding agents simultaneously.
You don't even need a computer; I've done meaningful work from my phone.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/boosting-developer-productivity-ai-2025#more-hours-in-the-day" aria-label="more-hours-in-the-day permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="more-hours-in-the-day"></span>More Hours in the Day</h3>
<p>This might sound hyperbolic, but async workflows have figuratively added more hours to my day.</p>
<p>The period I analyzed (September to December 2025) was unusually demanding.
During <strong>work hours</strong>, I was juggling three major open source projects: <a href="https://github.com/containers/kubernetes-mcp-server" rel="noopener" title="Link to https://github.com/containers/kubernetes-mcp-server" aria-label="Kubernetes MCP Server" target="_blank">Kubernetes MCP Server</a>, <a class="post-link " title="Kubernetes Client for Java: Fabric8 introduction" href="/kubernetes-client-java-fabric8-introduction">Fabric8 Kubernetes Client</a>, and <a class="post-link " title="Eclipse JKube introduction: Java tools and plugins for Kubernetes and OpenShift" href="/eclipse-jkube-introduction-kubernetes-openshift">Eclipse JKube</a>, plus various AI experiments.</p>
<p>Yet despite this workload, I managed to bring several neglected side projects back to life during my <strong>free time</strong>.
Projects that had been gathering dust for months suddenly became maintainable again.</p>
<p>The reason?
Async workflows let me queue up tasks and review results in spare moments.
A few minutes here and there, previously too short for meaningful coding, now add up to real progress.
Time that was once lost to waiting or context-switching has become productive.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/boosting-developer-productivity-ai-2025#ai-excels-at-grunt-work" aria-label="ai-excels-at-grunt-work permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="ai-excels-at-grunt-work"></span>AI Excels at Grunt Work</h3>
<p>Repetitive, tedious tasks that would take weeks of focused effort can be completed in days.
I refactored this entire blog asynchronously in a couple of days.
By my estimates, that would have taken two months of focused work before.
Technical debt removal has never been easier.</p>
<p>For example, I <a href="https://github.com/manusa/yakd/pull/156" rel="noopener" title="Link to https://github.com/manusa/yakd/pull/156" aria-label="migrated YAKD's frontend build" target="_blank">migrated YAKD's frontend build</a> from the deprecated create-react-app to Vite.
This involved 193 files, renaming <code>.js</code> to <code>.jsx</code>, migrating Jest to Vitest, and updating ESLint configuration.
What would have taken days of tedious, error-prone work was completed in minutes.</p>
<p>I also tackled ElectronIM's <a href="https://sonarcloud.io/summary/overall?id=manusa_electronim&amp;branch=main" rel="noopener" title="Link to https://sonarcloud.io/summary/overall?id=manusa_electronim&amp;branch=main" aria-label="SonarCloud" target="_blank">SonarCloud</a> issues using the GitHub Copilot Coding Agent.
The project had been clean for years, but as Sonar rules evolved, new issues appeared that weren't flagged before.
This is exactly the kind of low-priority work that would otherwise remain undone forever.
AI made it possible to tackle.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/boosting-developer-productivity-ai-2025#patterns-are-everything" aria-label="patterns-are-everything permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="patterns-are-everything"></span>Patterns Are Everything</h3>
<p>AI follows patterns.
If you show it a coding style, an architecture pattern, or an implementation approach, it will replicate it.
This makes AI incredibly effective for:</p>
<ul>
<li>Extending existing functionality following established patterns.</li>
<li>Applying consistent changes across a codebase.</li>
<li>Implementing features similar to existing ones.</li>
</ul>
<h3 class="heading"><a href="https://blog.marcnuri.com/boosting-developer-productivity-ai-2025#project-quality-matters-more-than-ever" aria-label="project-quality-matters-more-than-ever permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="project-quality-matters-more-than-ever"></span>Project Quality Matters More Than Ever</h3>
<p>Well-tested, well-structured projects get better results from AI.
Investing in <a class="post-link " title="What is Test-Driven Development (TDD)? A Practical Introduction" href="/test-driven-development-tdd-introduction">code quality</a> is no longer just about maintainability.
It's about AI-readiness.</p>
<h2 class="heading"><a href="https://blog.marcnuri.com/boosting-developer-productivity-ai-2025#conclusion" aria-label="conclusion permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="conclusion"></span>Conclusion</h2>
<p>2025 has been a year of transformation in how I approach software development.
The tools have matured, and the workflows have evolved from "AI as autocomplete" to "AI as parallel workforce."</p>
<p>The productivity gains are real, but they come with trade-offs.
The nature of the job is changing.
The risks of burnout and the implications for junior developers are concerns we need to address as an industry.</p>
<p>But for now, I'm excited about what's possible.
The GitHub contributions graph doesn't lie.
Something has fundamentally shifted, and I don't think we're going back.</p>
  ]]></content:encoded>
            <category>Artificial Intelligence</category>
            <enclosure url="https://blog.marcnuri.com/static/bbe61b2e5fb56bd875e405a1284d7070/869a6/marcnuri-cyborg.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Black Box vs White Box Testing: When to Use Each Approach]]></title>
            <link>https://blog.marcnuri.com/blackbox-whitebox-testing-comparison</link>
            <guid isPermaLink="false">https://blog.marcnuri.com/blackbox-whitebox-testing-comparison</guid>
            <pubDate>Mon, 06 Oct 2025 14:00:00 GMT</pubDate>
            <description><![CDATA[Discover key differences between blackbox and whitebox testing, and why blackbox should be your default for robust, maintainable test suites.]]></description>
            <content:encoded><![CDATA[
    <div><a href="https://blog.marcnuri.com/blackbox-whitebox-testing-comparison">Original post</a></div>
    <h2 class="heading"><a href="https://blog.marcnuri.com/blackbox-whitebox-testing-comparison#introduction" aria-label="introduction permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="introduction"></span>Introduction</h2>
<p><a class="category-link " title="Quality Engineering: Everything related to quality engineering, software testing, and test automation" aria-label="Software testing" href="/category/quality-engineering">Software testing</a> encompasses various methodologies designed to ensure application quality and reliability.
Two fundamental approaches are blackbox and whitebox testing, strategies that examine software from different perspectives.
Blackbox testing evaluates functionality without knowledge of internal code structure, while whitebox testing leverages detailed understanding of the application's implementation to verify logic and code paths.</p>
<p>While both methodologies have their place in software development, blackbox testing offers compelling advantages that make it the preferred approach for most scenarios.
Understanding these differences is essential for building robust, maintainable test suites that provide genuine regression protection.</p>
<p>In this article, I'll explain the core concepts of blackbox and whitebox testing, their key differences, and why blackbox testing should be the default choice for most development scenarios.</p>
<h2 class="heading"><a href="https://blog.marcnuri.com/blackbox-whitebox-testing-comparison#understanding-blackbox-testing" aria-label="understanding-blackbox-testing permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="understanding-blackbox-testing"></span>Understanding Blackbox Testing</h2>
<p>Blackbox testing is a methodology that examines an application's functionality without considering its internal code structure, design, or implementation details.
Testers interact with the software through its public interfaces, focusing exclusively on inputs provided to the system and the outputs it produces.</p>
<p>This approach treats the software as an opaque "black box" where internal workings remain hidden.
Tests validate whether the application behaves according to requirements without needing knowledge of how it is implemented.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/blackbox-whitebox-testing-comparison#key-characteristics" aria-label="key-characteristics permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="key-characteristics"></span>Key Characteristics</h3>
<p>Blackbox testing focuses on the behavior and contract of the software rather than how that behavior is achieved internally.
Tests interact only with public APIs, interfaces, and observable outputs.</p>
<p>This methodology decouples tests from implementation details, meaning the internal code can be refactored, optimized, or completely rewritten without breaking tests as long as the external behavior remains consistent.
Tests remain valid across implementation changes, providing genuine regression protection.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/blackbox-whitebox-testing-comparison#benefits-for-development" aria-label="benefits-for-development permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="benefits-for-development"></span>Benefits for Development</h3>
<p>Blackbox tests serve as living documentation of what the software should do from a user's perspective.
They validate that the software meets its specifications and requirements without being tied to specific implementation choices.</p>
<p>This approach enables confident refactoring since tests verify the contract rather than the implementation details.
Developers can improve performance, restructure code, or adopt better algorithms without modifying tests.</p>
<p>Blackbox tests are also easier for team members to understand because they focus on requirements and behavior rather than implementation specifics, making test suites more accessible across the team.</p>
<h2 class="heading"><a href="https://blog.marcnuri.com/blackbox-whitebox-testing-comparison#understanding-whitebox-testing" aria-label="understanding-whitebox-testing permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="understanding-whitebox-testing"></span>Understanding Whitebox Testing</h2>
<p>Whitebox testing examines the internal structure, design, and code implementation of the software under test.
This approach requires knowledge of the programming languages, frameworks, and implementation details used to develop the application.</p>
<p>The method derives its name from the transparency it provides into the application's internal workings.
Testers analyze code paths, conditions, loops, and logic flows to design test cases.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/blackbox-whitebox-testing-comparison#key-characteristics-and-limitations" aria-label="key-characteristics-and-limitations permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="key-characteristics-and-limitations"></span>Key Characteristics and Limitations</h3>
<p>Whitebox testing often relies on mocking frameworks to isolate units of code and verify interactions between components.
Tests must know how methods are implemented internally to set up appropriate mocks and expectations.</p>
<p>This creates tight coupling between tests and implementation details.
When the implementation changes, even if external behavior remains identical, whitebox tests often fail and require updates.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/blackbox-whitebox-testing-comparison#the-mocking-challenge" aria-label="the-mocking-challenge permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="the-mocking-challenge"></span>The Mocking Challenge</h3>
<p>Whitebox testing frequently depends on mocks and test doubles to replace dependencies and verify internal interactions.
While mocking has legitimate uses—such as testing error handling when dependencies fail or avoiding actual network calls to external systems—over-reliance on mocks to verify internal implementation details creates significant problems.</p>
<p>Tests using mocks verify that code calls specific methods with specific arguments rather than verifying actual behavior.
This means tests can pass even when the software doesn't work correctly.
Conversely, tests can fail even when the software works perfectly because implementation details changed.</p>
<p>Mocks couple tests directly to implementation choices, making refactoring risky and expensive.
Every internal restructuring requires updating numerous mock expectations across the test suite.</p>
<span class="post-image__pusher "></span><figure class="post-image "><span class="post-image__scrim"></span><a href="/static/2254ddbbfa2893e33f94178dd94e0963/6b7e6/blackbox-whitebox-cropped.jpg" class="post-image__link" title="An image of two boxes representing whitebox vs. blackbox"><span class="post-image__image-container"><div data-gatsby-image-wrapper="" class="gatsby-image-wrapper gatsby-image-wrapper-constrained post-image__image "><picture><source type="image/webp" data-srcset="/static/2254ddbbfa2893e33f94178dd94e0963/752c3/blackbox-whitebox-cropped.webp 200w,/static/2254ddbbfa2893e33f94178dd94e0963/50a91/blackbox-whitebox-cropped.webp 400w,/static/2254ddbbfa2893e33f94178dd94e0963/90364/blackbox-whitebox-cropped.webp 800w" sizes="(min-width: 800px) 800px, 100vw"><img data-gatsby-image-ssr="" data-main-image="" style="opacity: 1;" sizes="(min-width: 800px) 800px, 100vw" decoding="async" loading="lazy" data-src="/static/2254ddbbfa2893e33f94178dd94e0963/6b7e6/blackbox-whitebox-cropped.jpg" data-srcset="/static/2254ddbbfa2893e33f94178dd94e0963/023df/blackbox-whitebox-cropped.jpg 200w,/static/2254ddbbfa2893e33f94178dd94e0963/e192d/blackbox-whitebox-cropped.jpg 400w,/static/2254ddbbfa2893e33f94178dd94e0963/6b7e6/blackbox-whitebox-cropped.jpg 800w" alt="An image of two boxes representing whitebox vs. blackbox" src="https://blog.marcnuri.com/static/2254ddbbfa2893e33f94178dd94e0963/6b7e6/blackbox-whitebox-cropped.jpg" srcset="https://blog.marcnuri.com/static/2254ddbbfa2893e33f94178dd94e0963/023df/blackbox-whitebox-cropped.jpg 200w,https://blog.marcnuri.com/static/2254ddbbfa2893e33f94178dd94e0963/e192d/blackbox-whitebox-cropped.jpg 400w,https://blog.marcnuri.com/static/2254ddbbfa2893e33f94178dd94e0963/6b7e6/blackbox-whitebox-cropped.jpg 800w"></picture><script type="module">const t="undefined"!=typeof HTMLImageElement&&"loading"in HTMLImageElement.prototype;if(t){const t=document.querySelectorAll("img[data-main-image]");for(let e of t){e.dataset.src&&(e.setAttribute("src",e.dataset.src),e.removeAttribute("data-src")),e.dataset.srcset&&(e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset"));const t=e.parentNode.querySelectorAll("source[data-srcset]");for(let e of t)e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset");e.complete&&(e.style.opacity=1,e.parentNode.parentNode.querySelector("[data-placeholder-image]").style.opacity=0)}}</script></div></span></a></figure>
<h2 class="heading"><a href="https://blog.marcnuri.com/blackbox-whitebox-testing-comparison#why-blackbox-testing-should-be-preferred" aria-label="why-blackbox-testing-should-be-preferred permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="why-blackbox-testing-should-be-preferred"></span>Why Blackbox Testing Should Be Preferred</h2>
<p>Despite the widespread use of whitebox testing, blackbox testing offers clear advantages that make it the better default in most scenarios.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/blackbox-whitebox-testing-comparison#better-alignment-with-tdd" aria-label="better-alignment-with-tdd permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="better-alignment-with-tdd"></span>Better Alignment with TDD</h3>
<p><a class="post-link " title="What is Test-Driven Development (TDD)? A Practical Introduction" href="/test-driven-development-tdd-introduction">Test-Driven Development (TDD)</a> emphasizes writing tests before implementation to drive design decisions.
Blackbox testing naturally supports this workflow because tests specify what the code should do without requiring knowledge of how it will be implemented.</p>
<p>When writing blackbox tests first, developers focus on the specification and requirements rather than implementation details.
This leads to better API design and clearer contracts between components.</p>
<p>Whitebox testing can make TDD harder to practice effectively because it requires knowing implementation details before tests can be written.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/blackbox-whitebox-testing-comparison#implementation-independence" aria-label="implementation-independence permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="implementation-independence"></span>Implementation Independence</h3>
<p>The most significant advantage of blackbox testing is independence from implementation details.
Tests verify behavior and contracts, not specific code paths or internal interactions.</p>
<p>This independence enables fearless refactoring where developers can improve code structure, performance, or clarity without modifying tests.
Tests continue validating correctness as long as external behavior remains consistent.</p>
<p>By contrast, whitebox tests break whenever implementation changes, even when functionality is preserved.
This creates significant maintenance overhead and discourages beneficial refactoring.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/blackbox-whitebox-testing-comparison#reduced-test-maintenance" aria-label="reduced-test-maintenance permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="reduced-test-maintenance"></span>Reduced Test Maintenance</h3>
<p>Blackbox tests remain stable across refactoring and optimization efforts, reducing maintenance costs.
Tests written against public interfaces rarely need updates unless requirements change.</p>
<p>Whitebox tests require frequent updates as internal implementations evolve.
Every refactoring triggers cascading test failures that must be addressed before proceeding.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/blackbox-whitebox-testing-comparison#focus-on-specifications" aria-label="focus-on-specifications permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="focus-on-specifications"></span>Focus on Specifications</h3>
<p>Developers should write tests that verify specifications rather than implementation details.
Blackbox tests align with this goal by focusing on requirements and observable behavior.</p>
<p>Unit tests don't need mocks or internal knowledge to be effective.
They should test the contract a unit provides through its public interface.
Following <a class="tag-link " title="Best Practices" aria-label="best practices" href="/tag/best-practices">best practices</a> for test design ensures maintainable and valuable test suites.</p>
<p>These advantages become even more critical in modern development contexts where rapid iteration and continuous refactoring are essential practices.</p>
<h2 class="heading"><a href="https://blog.marcnuri.com/blackbox-whitebox-testing-comparison#blackbox-testing-in-ai-assisted-development-era" aria-label="blackbox-testing-in-ai-assisted-development-era permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="blackbox-testing-in-ai-assisted-development-era"></span>Blackbox Testing in the AI-Assisted Development Era</h2>
<p>The rise of <a class="category-link " title="Artificial Intelligence: Everything related to artificial intelligence (AI) and machine learning (ML)" aria-label="AI-assisted development" href="/category/ai">AI-assisted development</a> tools introduces new considerations for testing strategies.
AI assistants can modify implementations extensively while attempting to maintain functional behavior.
When paired with blackbox tests, these AI-generated changes face immediate validation against specifications without requiring test modifications.</p>
<p>Blackbox tests act as a safety net, verifying that AI-generated code meets requirements while remaining stable through implementation changes.
The AI can refactor, optimize, or rewrite logic while tests confirm that external behavior remains correct.
This separation between specification and implementation becomes critical when AI assistants frequently suggest optimizations and refactorings.</p>
<p>Whitebox tests become liabilities in AI-assisted development because they break with every implementation change the AI makes.
Developers end up spending excessive time reviewing both code and test changes to ensure everything still works correctly.
As AI development tools become more sophisticated and autonomous, blackbox tests provide the resilience needed to support rapid iteration while maintaining quality standards.</p>
<p>For real-world examples of AI-assisted development with blackbox testing, see <a class="post-link " title="Boosting My Developer Productivity with AI in 2025" href="/boosting-developer-productivity-ai-2025">how AI agents boosted developer productivity in 2025</a>.</p>
<h2 class="heading"><a href="https://blog.marcnuri.com/blackbox-whitebox-testing-comparison#when-to-consider-whitebox-testing" aria-label="when-to-consider-whitebox-testing permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="when-to-consider-whitebox-testing"></span>When to Consider Whitebox Testing</h2>
<p>While blackbox testing should be your default approach, whitebox testing remains valuable and necessary in specific scenarios where examining internal implementation reveals issues that external behavior cannot expose.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/blackbox-whitebox-testing-comparison#limitations-of-pure-blackbox-testing" aria-label="limitations-of-pure-blackbox-testing permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="limitations-of-pure-blackbox-testing"></span>The Limitations of Pure Blackbox Testing</h3>
<p>Completely ignoring source code and assuming optimal implementation is not a reliable strategy for building robust software.
Testing every possible input combination to verify outputs is often impractical or impossible, especially with unbounded inputs.</p>
<p>Consider this function:</p>
<div class="code" style="background:#232323"><pre style="display:block;overflow-x:auto;padding:0.5em;background:#232323;color:#e6e1dc"><code class="language-go" style="white-space:pre"><span style="color:#c26230">package</span><span> main
</span>
<span></span><span class="hljs-function" style="color:#c26230">func</span><span class="hljs-function"> </span><span class="hljs-function" style="color:#ffc66d">product</span><span class="hljs-function" style="color:#d0d0ff">(a, b </span><span class="hljs-function" style="color:#c26230">int</span><span class="hljs-function" style="color:#d0d0ff">)</span><span class="hljs-function"> </span><span class="hljs-function" style="color:#ffc66d">int</span><span> {
</span><span>    </span><span style="color:#c26230">if</span><span> a == </span><span style="color:#a5c261">42</span><span> {
</span><span>        </span><span style="color:#c26230">return</span><span> </span><span style="color:#a5c261">0</span><span>  </span><span style="color:#bc9458;font-style:italic">// Bug: violates specification</span><span>
</span>    }
<span>    </span><span style="color:#c26230">return</span><span> a * b
</span>}</code></pre></div>
<p>A blackbox test that validates typical multiplication cases (<code>product(2, 3) == 6</code>, <code>product(-1, 5) == -5</code>) would pass, yet the implementation violates the specification by returning incorrect results for one input value.
Without examining the source code, discovering that <code>42</code> produces incorrect results requires either exhaustive testing of all possible inputs (impossible with unbounded integers) or extraordinarily lucky test case selection.</p>
<div class="admonition admonition__tip"><p class="admonition-title"><i class="admonition-title-icon fa-regular fa-lightbulb"></i>Tip</p><div class="admonition-content">
<p>This example demonstrates why some level of code inspection remains valuable even when primarily using blackbox approaches.</p>
</div></div>
<h4 class="heading"><a href="https://blog.marcnuri.com/blackbox-whitebox-testing-comparison#appropriate-use-cases-for-whitebox-testing" aria-label="appropriate-use-cases-for-whitebox-testing permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="appropriate-use-cases-for-whitebox-testing"></span>Appropriate Use Cases for Whitebox Testing</h4>
<p>Whitebox testing is particularly valuable in scenarios such as:</p>
<ul>
<li><strong>Complex algorithms with hidden branches</strong>: Internal conditions may only surface through code inspection (e.g., the special case handling in the multiplication example).</li>
<li><strong>Performance-critical code</strong>: Verifying that optimizations are implemented correctly may require checking internal code paths.</li>
<li><strong>Security-sensitive logic</strong>: Authentication flows, encryption, and access control often depend on implementation details that should be verified directly.</li>
<li><strong>Code coverage goals</strong>: Designing tests to exercise all branches and conditions requires knowledge of internal structure.</li>
</ul>
<h4 class="heading"><a href="https://blog.marcnuri.com/blackbox-whitebox-testing-comparison#balancing-both-approaches" aria-label="balancing-both-approaches permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="balancing-both-approaches"></span>Balancing Both Approaches</h4>
<p>The most effective strategy combines both methodologies:</p>
<ol>
<li>Start with blackbox tests that verify specifications and requirements through public interfaces.</li>
<li>Use code coverage tools to identify untested paths and conditions.
Then, add targeted test cases to address the gaps.</li>
<li>Supplement with targeted whitebox tests for concerns that cannot be validated externally.</li>
</ol>
<p>Even when whitebox analysis identifies test cases, consider implementing them through public interfaces when possible.</p>
<div class="admonition admonition__tip"><p class="admonition-title"><i class="admonition-title-icon fa-regular fa-lightbulb"></i>Tip</p><div class="admonition-content">
<p>The multiplication example could be tested as <code>product(42, 5) == 210</code> once the bug is discovered, making it a blackbox test informed by whitebox analysis.</p>
</div></div>
<p>This approach combines the stability of blackbox tests with the completeness of targeted whitebox coverage.</p>
<h2 class="heading"><a href="https://blog.marcnuri.com/blackbox-whitebox-testing-comparison#conclusion" aria-label="conclusion permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="conclusion"></span>Conclusion</h2>
<p>While blackbox and whitebox testing complement each other, the advantages of blackbox testing make it the superior default for modern <a class="tag-link " title="Programming" aria-label="software development" href="/tag/programming">software development</a>.
Blackbox testing focuses on specifications rather than implementation details, enabling confident refactoring and reducing maintenance overhead.</p>
<p>The coupling that whitebox testing introduces through mocks and internal knowledge creates brittle test suites that break with every refactoring.
In the era of AI-assisted development, blackbox tests provide an essential protection layer that validates AI-generated changes without requiring constant modifications.</p>
<p>Developers should write unit tests that verify specifications through public interfaces.
This produces maintainable, resilient test suites that provide genuine regression protection while supporting rapid iteration and continuous improvement.</p>
  ]]></content:encoded>
            <category>Quality Engineering</category>
            <enclosure url="https://blog.marcnuri.com/static/53219e6888d7ead0279859676d25369b/b26d3/blackbox-whitebox.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Fabric8 Kubernetes Client 7.4 is now available!]]></title>
            <link>https://blog.marcnuri.com/fabric8-kubernetes-client-7-4</link>
            <guid isPermaLink="false">https://blog.marcnuri.com/fabric8-kubernetes-client-7-4</guid>
            <pubDate>Mon, 08 Sep 2025 16:00:00 GMT</pubDate>
            <description><![CDATA[Fabric8 Kubernetes Client 7.4 is available! Check out the major changes and learn how you can contribute.]]></description>
            <content:encoded><![CDATA[
    <div><a href="https://blog.marcnuri.com/fabric8-kubernetes-client-7-4">Original post</a></div>
    <div class="admonition admonition__note"><p class="admonition-title"><i class="admonition-title-icon fa-solid fa-circle-info"></i>Note</p><div class="admonition-content">A newer version of Fabric8 Kubernetes Client is available, jump to <a class="post-link " title="Fabric8 Kubernetes Client 7.6 is now available!" href="/fabric8-kubernetes-client-7-6">Fabric8 Kubernetes Client 7.6 announcement</a>.</div></div>
<p>On behalf of the <a class="post-link " title="Kubernetes Client for Java: Fabric8 introduction" href="/kubernetes-client-java-fabric8-introduction">Fabric8</a>
team and everyone who has contributed, I'm happy to announce that the Fabric8 Kubernetes Client <code>7.4.0</code> has been
<a href="https://github.com/fabric8io/kubernetes-client/releases/tag/v7.4.0" rel="noopener" title="Link to https://github.com/fabric8io/kubernetes-client/releases/tag/v7.4.0" aria-label="released" target="_blank">released</a> and is now available from
<a href="https://repo1.maven.org/maven2/io/fabric8/kubernetes-client/7.4.0/" rel="noopener" title="Link to https://repo1.maven.org/maven2/io/fabric8/kubernetes-client/7.4.0/" aria-label="Maven Central" target="_blank">Maven Central</a> 🎉.</p>
<p>This marks the fourth minor release of the Fabric8 Kubernetes Client 7, bringing new features, bug fixes, and improvements while keeping the breaking changes minimal.</p>
<p>Thanks to all of you who have contributed with issue reports, pull requests, feedback, and spreading the word with blogs, videos, comments, and so on.
We really appreciate your help, keep it up!</p>
<h2 class="heading"><a href="https://blog.marcnuri.com/fabric8-kubernetes-client-7-4#whats-new" aria-label="whats-new permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="whats-new"></span>What's new?</h2>
<p>Without further ado, let's have a look at the most significant updates:</p>
<ul>
<li><a class="post-link " title="Link to the Testing Improvements section" href="/fabric8-kubernetes-client-7-4#testing-improvements">Enhanced testing framework with mock client improvements</a></li>
<li><a class="post-link " title="Link to the Informer Stability section" href="/fabric8-kubernetes-client-7-4#informer-stability">Improved informer and watch operation stability</a></li>
<li><a class="post-link " title="Link to the CRD Improvements section" href="/fabric8-kubernetes-client-7-4#crd-improvements">CRD generation enhancements with annotation support</a></li>
<li><a class="post-link " title="Link to the Connection Stability section" href="/fabric8-kubernetes-client-7-4#connection-stability">Better WebSocket and connection handling</a></li>
<li>🐛 Many other bug fixes and minor improvements</li>
</ul>
<p>You can find the full changelog for this version in our GitHub <a href="https://github.com/fabric8io/kubernetes-client/releases/tag/v7.4.0" rel="noopener" title="Link to https://github.com/fabric8io/kubernetes-client/releases/tag/v7.4.0" aria-label="release page" target="_blank">release page</a>.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/fabric8-kubernetes-client-7-4#testing-improvements" aria-label="testing-improvements permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="testing-improvements"></span>Enhanced testing framework with mock client improvements</h3>
<p>The testing experience has been significantly enhanced in this release.
The <code>@EnableKubernetesMockClient</code> annotation now works properly with nested tests, resolving JUnit integration issues that were affecting test organization and execution.</p>
<p>Additionally, the <code>KubeApiTest</code> inheritance from base test classes has been fixed, providing better test framework compatibility and making it easier to structure your test suites.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/fabric8-kubernetes-client-7-4#informer-stability" aria-label="informer-stability permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="informer-stability"></span>Improved informer and watch operation stability</h3>
<p>This release brings improvements to the Informer API.
You can now use <code>Informer.isWatching()</code> to see the underlying Watch state, providing better visibility into watch operations and helping with debugging and monitoring of long-running watch connections.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/fabric8-kubernetes-client-7-4#crd-improvements" aria-label="crd-improvements permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="crd-improvements"></span>CRD generation enhancements with annotation support</h3>
<p>The CRD generator has been extended with support for <code>@Annotations</code> and <code>@Labels</code> in CRD generation. The CRD generator now includes metadata from these annotations, making it easier to generate comprehensive Custom Resource Definitions with proper annotations and labels.</p>
<p>The java-generator has also been enhanced to support the use of existing enumerations through extended <code>existingJavaTypes</code> configuration.</p>
<h3 class="heading"><a href="https://blog.marcnuri.com/fabric8-kubernetes-client-7-4#connection-stability" aria-label="connection-stability permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="connection-stability"></span>Better WebSocket and connection handling</h3>
<p>Several critical fixes have been implemented to improve connection stability:</p>
<ul>
<li>Fixed potential NPE in OkHttp WebSocket handling</li>
<li>Ensured that streams are properly notified of errors</li>
<li>Changed rolling update handling to use JSON merge patch to avoid 422 errors during deployment updates</li>
</ul>
<p>These improvements make the client more reliable when dealing with long-running connections and deployment operations.</p>
<h2 class="heading"><a href="https://blog.marcnuri.com/fabric8-kubernetes-client-7-4#using-this-release" aria-label="using-this-release permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="using-this-release"></span>Using this release</h2>
<p>If your project is based on Maven, you just need to add the Fabric8 Kubernetes Client to your Maven dependencies:</p>
<div class="code" style="background:#232323"><pre style="display:block;overflow-x:auto;padding:0.5em;background:#232323;color:#e6e1dc"><code class="language-xml" style="white-space:pre"><span style="color:#e8bf6a">&lt;</span><span style="color:#e8bf6a">dependency</span><span style="color:#e8bf6a">&gt;</span><span>
</span><span>  </span><span style="color:#e8bf6a">&lt;</span><span style="color:#e8bf6a">groupId</span><span style="color:#e8bf6a">&gt;</span><span>io.fabric8</span><span style="color:#e8bf6a">&lt;/</span><span style="color:#e8bf6a">groupId</span><span style="color:#e8bf6a">&gt;</span><span>
</span><span>  </span><span style="color:#e8bf6a">&lt;</span><span style="color:#e8bf6a">artifactId</span><span style="color:#e8bf6a">&gt;</span><span>kubernetes-client</span><span style="color:#e8bf6a">&lt;/</span><span style="color:#e8bf6a">artifactId</span><span style="color:#e8bf6a">&gt;</span><span>
</span><span>  </span><span style="color:#e8bf6a">&lt;</span><span style="color:#e8bf6a">version</span><span style="color:#e8bf6a">&gt;</span><span>7.4.0</span><span style="color:#e8bf6a">&lt;/</span><span style="color:#e8bf6a">version</span><span style="color:#e8bf6a">&gt;</span><span>
</span><span></span><span style="color:#e8bf6a">&lt;/</span><span style="color:#e8bf6a">dependency</span><span style="color:#e8bf6a">&gt;</span></code></pre></div>
<p>If your project is based on Gradle, you just need to add the Fabric8 Kubernetes Client to your Gradle dependencies:</p>
<div class="code" style="background:#232323"><pre style="display:block;overflow-x:auto;padding:0.5em;background:#232323;color:#e6e1dc"><code class="language-groovy" style="white-space:pre"><span>dependencies {
</span><span>  api </span><span style="color:#a5c261">"io.fabric8:kubernetes-client:7.4.0"</span><span>
</span>}</code></pre></div>
<p>Once your project is ready, you can create a new instance of the client to perform operations.
In the following code snippet, I show you how to instantiate the client and retrieve a list of Pods:</p>
<div class="code" style="background:#232323"><pre style="display:block;overflow-x:auto;padding:0.5em;background:#232323;color:#e6e1dc"><code class="language-java" style="white-space:pre"><span style="color:#c26230">try</span><span> (KubernetesClient client = </span><span style="color:#c26230">new</span><span> KubernetesClientBuilder().build()) {
</span>  client.pods().list().getItems().forEach(p -&gt; System.out.println(p.getMetadata().getName()));
<!-- -->}</code></pre></div>
<h2 class="heading"><a href="https://blog.marcnuri.com/fabric8-kubernetes-client-7-4#how-can-you-help" aria-label="how-can-you-help permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="how-can-you-help"></span>How can you help?</h2>
<p>If you're interested in helping out and are a first-time contributor, check out
the <a href="https://github.com/fabric8io/kubernetes-client/labels/good%20first%20issue" rel="noopener" title="Link to https://github.com/fabric8io/kubernetes-client/labels/good%20first%20issue" aria-label="&quot;good first issue&quot;" target="_blank">"good first issue"</a> tag in the issue repository.
We've tagged extremely easy issues so that you can get started contributing to Open Source.</p>
<p>We're also excited to read articles and posts mentioning our project and sharing the user experience.
Giving a star to the project, and spreading the word in general, helps us reach more users and broaden the
feedback. Feedback is the only way to improve.</p>
<p><a href="https://github.com/fabric8io/kubernetes-client" rel="noopener" title="Link to https://github.com/fabric8io/kubernetes-client" aria-label="Project Page" target="_blank">Project Page</a> |
<a href="https://github.com/fabric8io/kubernetes-client/issues" rel="noopener" title="Link to https://github.com/fabric8io/kubernetes-client/issues" aria-label="Issues" target="_blank">Issues</a> |
<a href="https://github.com/fabric8io/kubernetes-client/discussions" rel="noopener" title="Link to https://github.com/fabric8io/kubernetes-client/discussions" aria-label="Discussions" target="_blank">Discussions</a> |
<a href="https://gitter.im/fabric8io/kubernetes-client" rel="noopener" title="Link to https://gitter.im/fabric8io/kubernetes-client" aria-label="Gitter" target="_blank">Gitter</a> |
<a href="https://stackoverflow.com/questions/tagged/fabric8" rel="noopener" title="Link to https://stackoverflow.com/questions/tagged/fabric8" aria-label="Stack Overflow" target="_blank">Stack Overflow</a></p>
<span class="post-image__pusher "></span><figure class="post-image "><span class="post-image__scrim"></span><a href="/static/eb11f2059915fc28b71c76eff27dc487/539a4/fabric8-logo.png" class="post-image__link" title="The logo of Fabric8 Kubernetes Client"><span class="post-image__image-container"><div data-gatsby-image-wrapper="" class="gatsby-image-wrapper gatsby-image-wrapper-constrained post-image__image "><picture><source type="image/webp" data-srcset="/static/eb11f2059915fc28b71c76eff27dc487/a66c2/fabric8-logo.webp 269w,/static/eb11f2059915fc28b71c76eff27dc487/3860d/fabric8-logo.webp 539w,/static/eb11f2059915fc28b71c76eff27dc487/418ed/fabric8-logo.webp 1077w" sizes="(min-width: 1077px) 1077px, 100vw"><img data-gatsby-image-ssr="" data-main-image="" style="opacity: 1;" sizes="(min-width: 1077px) 1077px, 100vw" decoding="async" loading="lazy" data-src="/static/eb11f2059915fc28b71c76eff27dc487/539a4/fabric8-logo.png" data-srcset="/static/eb11f2059915fc28b71c76eff27dc487/53e69/fabric8-logo.png 269w,/static/eb11f2059915fc28b71c76eff27dc487/f0c2e/fabric8-logo.png 539w,/static/eb11f2059915fc28b71c76eff27dc487/539a4/fabric8-logo.png 1077w" alt="The logo of Fabric8 Kubernetes Client" src="https://blog.marcnuri.com/static/eb11f2059915fc28b71c76eff27dc487/539a4/fabric8-logo.png" srcset="https://blog.marcnuri.com/static/eb11f2059915fc28b71c76eff27dc487/53e69/fabric8-logo.png 269w,https://blog.marcnuri.com/static/eb11f2059915fc28b71c76eff27dc487/f0c2e/fabric8-logo.png 539w,https://blog.marcnuri.com/static/eb11f2059915fc28b71c76eff27dc487/539a4/fabric8-logo.png 1077w"></picture><script type="module">const t="undefined"!=typeof HTMLImageElement&&"loading"in HTMLImageElement.prototype;if(t){const t=document.querySelectorAll("img[data-main-image]");for(let e of t){e.dataset.src&&(e.setAttribute("src",e.dataset.src),e.removeAttribute("data-src")),e.dataset.srcset&&(e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset"));const t=e.parentNode.querySelectorAll("source[data-srcset]");for(let e of t)e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset");e.complete&&(e.style.opacity=1,e.parentNode.parentNode.querySelector("[data-placeholder-image]").style.opacity=0)}}</script></div></span></a></figure>
  ]]></content:encoded>
            <category>Cloud Native</category>
            <enclosure url="https://blog.marcnuri.com/static/b9dbe50d753954e05cf28f5e3aa1c88a/baaed/fabric8.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Kubernetes MCP Server Joins the Containers Organization!]]></title>
            <link>https://blog.marcnuri.com/kubernetes-mcp-server-containers-organization</link>
            <guid isPermaLink="false">https://blog.marcnuri.com/kubernetes-mcp-server-containers-organization</guid>
            <pubDate>Thu, 24 Jul 2025 07:00:00 GMT</pubDate>
            <description><![CDATA[The Kubernetes MCP Server GitHub repository has moved to the Containers organization.]]></description>
            <content:encoded><![CDATA[
    <div><a href="https://blog.marcnuri.com/kubernetes-mcp-server-containers-organization">Original post</a></div>
    <p>I'm thrilled to announce that the Kubernetes MCP Server has officially migrated to the <a href="https://github.com/containers" rel="noopener" title="GitHub containers organization home" aria-label="github.com/containers" target="_blank">github.com/containers</a> GitHub organization!</p>
<span class="post-image__pusher "></span><figure class="post-image "><span class="post-image__scrim"></span><a href="/static/ff3668f821517e5aa36e6543d4923c6d/ba8e4/containers-full-horiz.png" class="post-image__link" title="The logo of Containers organization"><span class="post-image__image-container"><div data-gatsby-image-wrapper="" class="gatsby-image-wrapper gatsby-image-wrapper-constrained post-image__image "><picture><source type="image/webp" data-srcset="/static/ff3668f821517e5aa36e6543d4923c6d/6b414/containers-full-horiz.webp 242w,/static/ff3668f821517e5aa36e6543d4923c6d/52c13/containers-full-horiz.webp 484w,/static/ff3668f821517e5aa36e6543d4923c6d/06c86/containers-full-horiz.webp 967w" sizes="(min-width: 967px) 967px, 100vw"><img data-gatsby-image-ssr="" data-main-image="" style="opacity: 1;" sizes="(min-width: 967px) 967px, 100vw" decoding="async" loading="lazy" data-src="/static/ff3668f821517e5aa36e6543d4923c6d/ba8e4/containers-full-horiz.png" data-srcset="/static/ff3668f821517e5aa36e6543d4923c6d/40baf/containers-full-horiz.png 242w,/static/ff3668f821517e5aa36e6543d4923c6d/48ff9/containers-full-horiz.png 484w,/static/ff3668f821517e5aa36e6543d4923c6d/ba8e4/containers-full-horiz.png 967w" alt="The logo of Containers organization" src="https://blog.marcnuri.com/static/ff3668f821517e5aa36e6543d4923c6d/ba8e4/containers-full-horiz.png" srcset="https://blog.marcnuri.com/static/ff3668f821517e5aa36e6543d4923c6d/40baf/containers-full-horiz.png 242w,https://blog.marcnuri.com/static/ff3668f821517e5aa36e6543d4923c6d/48ff9/containers-full-horiz.png 484w,https://blog.marcnuri.com/static/ff3668f821517e5aa36e6543d4923c6d/ba8e4/containers-full-horiz.png 967w"></picture><script type="module">const t="undefined"!=typeof HTMLImageElement&&"loading"in HTMLImageElement.prototype;if(t){const t=document.querySelectorAll("img[data-main-image]");for(let e of t){e.dataset.src&&(e.setAttribute("src",e.dataset.src),e.removeAttribute("data-src")),e.dataset.srcset&&(e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset"));const t=e.parentNode.querySelectorAll("source[data-srcset]");for(let e of t)e.setAttribute("srcset",e.dataset.srcset),e.removeAttribute("data-srcset");e.complete&&(e.style.opacity=1,e.parentNode.parentNode.querySelector("[data-placeholder-image]").style.opacity=0)}}</script></div></span></a></figure>
<p><strong>New Repository Location</strong>:</p>
<p>👉 <a href="https://github.com/containers/kubernetes-mcp-server" rel="noopener" title="Kubernetes MCP Server GitHub repository" aria-label="github.com/containers/kubernetes-mcp-server" target="_blank">github.com/containers/kubernetes-mcp-server</a></p>
<h2 class="heading"><a href="https://blog.marcnuri.com/kubernetes-mcp-server-containers-organization#why-the-move" aria-label="why-the-move permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="why-the-move"></span>Why the Move?</h2>
<p>The Kubernetes MCP Server has grown far beyond its origins as a <a class="category-link " title="Personal: Personal posts and articles" aria-label="personal project" href="/category/personal">personal project</a>.
It is now a powerful, flexible, and feature-complete <a class="tag-link " title="Model Context Protocol (MCP)" aria-label="Model Context Protocol (MCP)" href="/tag/mcp">Model Context Protocol (MCP)</a> server,
enabling seamless interaction with <a class="tag-link " title="Kubernetes" aria-label="Kubernetes" href="/tag/kubernetes">Kubernetes</a> and <a class="tag-link " title="OpenShift" aria-label="OpenShift" href="/tag/openshift">OpenShift</a> clusters.
With over 10 contributors, 400 stars, 60 forks, 46 releases, and thousands of weekly downloads,
it has become a key component in the  MCP ecosystem.</p>
<p>Our main goal for this migration is to foster a more neutral, community-driven, and inclusive collaboration environment.
By joining the well-established Containers organization, we're better positioned to:</p>
<ul>
<li>Welcome <strong>new contributors</strong> and maintainers from diverse backgrounds and organizations.</li>
<li>Signal that the project is <strong>open</strong> for community governance and input.</li>
<li>Ensure the project's long-term <strong>sustainability</strong> and growth.</li>
<li>Accelerate the <strong>adoption</strong> of MCP as a Kubernetes and OpenShift interface.</li>
</ul>
<h2 class="heading"><a href="https://blog.marcnuri.com/kubernetes-mcp-server-containers-organization#whats-next" aria-label="whats-next permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="whats-next"></span>What’s Next?</h2>
<p>Moving to the Containers organization is just the start.
With a strong foundation in place and your support, we'll be shipping new features, improving the user experience, and building tools that matter for the Kubernetes, OpenShift, and AI communities.</p>
<p>If you’re building tools for AI agents that need to interact with Kubernetes,
or if you're just curious about Model Context Protocol,
this is the time to get involved.</p>
<h2 class="heading"><a href="https://blog.marcnuri.com/kubernetes-mcp-server-containers-organization#get-involved" aria-label="get-involved permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="get-involved"></span>Get Involved!</h2>
<p>No matter your background, your involvement is welcome and valued! Here’s how you can participate:</p>
<ul>
<li>⭐ <strong>Star the repo</strong>: <a href="https://github.com/containers/kubernetes-mcp-server" rel="noopener" title="Kubernetes MCP Server GitHub repository" aria-label="github.com/containers/kubernetes-mcp-server" target="_blank">github.com/containers/kubernetes-mcp-server</a></li>
<li>🧑‍💻 <strong>Contribute</strong>: Submit features, bugfixes, issues, documentation, or integrations.</li>
<li>📣 <strong>Spread the word</strong>: Share this announcement, the repository, and your experiences with the Kubernetes MCP Server.</li>
</ul>
<h2 class="heading"><a href="https://blog.marcnuri.com/kubernetes-mcp-server-containers-organization#you-might-also-like" aria-label="you-might-also-like permalink" class="anchor"><i class="anchor__link fa-solid fa-link"></i></a><span id="you-might-also-like"></span>You Might Also Like</h2>
<ul>
<li><a class="post-link " title="Introduction to the Model Context Protocol (MCP): The Future of AI Integration" href="/model-context-protocol-mcp-introduction">Introduction to the Model Context Protocol (MCP): The Future of AI Integration</a></li>
<li><a class="post-link " title="Giving Superpowers to Small Language Models with Model Context Protocol (MCP)" href="/giving-superpowers-to-small-language-models-with-mcp">Giving Superpowers to Small Language Models with Model Context Protocol (MCP)</a></li>
<li><a class="post-link " title="Introducing Goose, the on-machine AI agent" href="/goose-on-machine-ai-agent-cli-introduction">Introducing Goose, the on-machine AI agent</a></li>
</ul>
  ]]></content:encoded>
            <category>Artificial Intelligence</category>
            <category>Personal</category>
            <enclosure url="https://blog.marcnuri.com/static/24f387c11ab766769009f4d4d36d4097/b26d3/android-mechanic.jpg" length="0" type="image/jpg"/>
        </item>
    </channel>
</rss>