GitOps for Homelabs: How ArgoCD App-of-Apps Scales Your Cluster

Managing a homelab Kubernetes cluster by hand eventually breaks you. Not in a dramatic way — in a slow, grinding way. You tweak a deployment to fix something urgent, forget to commit the change, and three months later you can’t reproduce your own setup. A node dies and you’re reverse-engineering what was running on it from kubectl get output and fuzzy memory.

I hit that wall. The fix was GitOps, specifically ArgoCD’s app-of-apps pattern. This post is about how I structured it, what I got wrong the first time, and what actually works in a homelab context where you’re the only engineer and iteration speed matters.

Why App-of-Apps and Not Just Helm

ArgoCD has several ways to manage applications. The simplest is pointing ArgoCD at a single app manifest — done. That works fine for three apps. It doesn’t scale when you have fifteen namespaces and forty-plus workloads, each with their own sync policies, health checks, and override values.

The app-of-apps pattern solves this with a hierarchy: one parent Application that ArgoCD watches. That parent’s source is a directory of child Application manifests. When ArgoCD syncs the parent, it discovers and creates all the children. Each child then manages its own workload independently.

The result: your entire cluster state is defined in Git. Adding a new app is a git push. Removing one is a git rm and a sync. Disaster recovery becomes argocd app sync root-app instead of a weekend of YAML archaeology.

Repo Structure That Actually Works

I tried a few layouts before settling on one I could actually navigate. The key insight is separating “ArgoCD app definitions” from “workload manifests”:

kubernetes/
├── apps/              # ArgoCD Application CRDs (the app-of-apps layer)
│   ├── root-app.yaml  # Parent: points to kubernetes/apps/
│   ├── monitoring.yaml
│   ├── ingress.yaml
│   ├── ai-llm.yaml
│   └── media.yaml
└── workloads/         # Actual manifests (what ArgoCD deploys)
    ├── monitoring/
    │   ├── prometheus/
    │   └── grafana/
    ├── ingress/
    │   └── traefik/
    └── ai-llm/
        ├── ollama/
        └── qdrant/

apps/ is what ArgoCD watches to discover applications. workloads/ is what those applications deploy. They live in the same repo, which keeps everything together and simplifies access control.

The Root App

The parent application is the entry point. You create this one manually (or via CLI), and from then on ArgoCD takes over:

# kubernetes/apps/root-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: root-app
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: default
  source:
    repoURL: https://github.com/your-org/homelab
    targetRevision: main
    path: kubernetes/apps
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

The prune: true flag is important — it means if you delete a child Application manifest from Git, ArgoCD removes the app (and its resources) from the cluster. Without it, stale apps accumulate. selfHeal: true means ArgoCD will revert manual cluster changes back to Git state, which is the whole point.

Child App Manifests

Each child Application points to a workload directory. Here’s what a typical one looks like:

# kubernetes/apps/monitoring.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: monitoring
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: default
  source:
    repoURL: https://github.com/your-org/homelab
    targetRevision: main
    path: kubernetes/workloads/monitoring
  destination:
    server: https://kubernetes.default.svc
    namespace: monitoring
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

CreateNamespace=true means ArgoCD creates the namespace if it doesn’t exist. This matters for bootstrapping — you shouldn’t need to pre-create namespaces manually.

The workload directory can contain raw YAML manifests, a kustomization.yaml, or a Helm chart (via the helm source type). I use raw YAML for most things and Helm where upstream charts make it the obvious choice.

Workload Layout Inside Each App

For a typical workload like a deployment + service + ingress, I keep files flat within the app directory:

kubernetes/workloads/ai-llm/ollama/
├── namespace.yaml
├── deployment.yaml
├── service.yaml
├── ingress.yaml
└── pvc.yaml

ArgoCD will discover and apply all YAML files in the path recursively. You don’t need a kustomization file unless you want explicit ordering or overlays. For simple workloads, flat files are easier to navigate and less to maintain.

One practical note: if you need a specific apply order (like a Namespace before a Deployment), ArgoCD respects resource sync waves via annotation:

metadata:
  annotations:
    argocd.argoproj.io/sync-wave: "0"  # Lower = applies first

Namespaces get wave 0, CRDs wave 1, everything else default (0 or unset). I only bother with this when ArgoCD actually complains about ordering.

Ingress Health: The “Progressing” Trap

One gotcha I hit: ArgoCD was stuck showing Progressing for any app with an Ingress, even when the workload was fully running. The health check for Ingress resources checks whether the status.loadBalancer.ingress field is populated — and if your ingress controller doesn’t write that field, ArgoCD waits forever.

The fix for Traefik is to set the publishedService argument so Traefik copies its LoadBalancer status into Ingress objects:

# In your Traefik Helm values or deployment args
args:
  - --providers.kubernetesIngress.publishedService=traefik/traefik

Once that’s set, Ingress objects get a populated status.loadBalancer and ArgoCD’s health check passes. This is a common gotcha with any ingress controller that doesn’t do this automatically.

Bootstrapping a New Cluster

The real test of GitOps is: “Can I rebuild this from scratch?” With app-of-apps, the bootstrap sequence is short:

# 1. Install ArgoCD
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

# 2. Apply the root app
kubectl apply -f kubernetes/apps/root-app.yaml

# 3. Trigger initial sync
argocd app sync root-app

ArgoCD discovers all child apps from the apps/ directory, creates them, and syncs each one. Within a few minutes the cluster converges to Git state. There’s no manual “now deploy this, then that” orchestration.

The parts that aren’t in Git — cluster-level things like StorageClasses, TLS issuers, sealed secrets — I handle as prerequisites documented separately. But application workloads: fully reproducible.

Dealing With Secrets

The one thing you cannot just commit is secrets. I use SealedSecrets for this. The workflow:

  1. Create a regular Kubernetes Secret manifest
  2. Encrypt it with kubeseal against the cluster’s public key
  3. Commit the SealedSecret manifest — it’s safe to store in Git
  4. ArgoCD deploys the SealedSecret; the controller decrypts it into a real Secret in-cluster
# Seal a secret for a specific namespace
kubectl create secret generic my-app-secret 
  --from-literal=password=supersecret 
  --dry-run=client -o yaml | 
  kubeseal --namespace my-app --format yaml > sealed-secret.yaml

The sealed output looks like garbage — it’s just an encrypted blob. Commit that. The actual credentials never touch your repo.

Back up the SealedSecrets controller’s encryption key somewhere safe (I use a password manager). If you lose it and need to rebuild, you’ll have to re-seal everything.

Sync Policies: Auto vs. Manual

Not everything should auto-sync. I use automated sync for stateless workloads and anything where drift is actively bad. For databases and stateful workloads I prefer manual sync, so I have to explicitly approve changes before they apply:

syncPolicy:
  # No 'automated' block = manual sync only
  syncOptions:
    - CreateNamespace=true

This way a bad migration doesn’t auto-apply at 2am. ArgoCD will still tell you the app is OutOfSync, but it won’t act without you.

The general rule I follow: if the app has a PVC or database, manual sync. If it’s stateless and idempotent, auto-sync.

Repo Connection and Auth

ArgoCD needs read access to your Git repo. For a private repo, you add credentials via the CLI or UI:

argocd repo add https://github.com/your-org/homelab 
  --username your-user 
  --password your-pat

Use a Personal Access Token with minimal scope — read-only on the repo contents is sufficient. ArgoCD only reads from Git; it doesn’t push anything back.

What I’d Do Differently

Start with the structure, not the apps. I initially just pointed ArgoCD at whatever manifests I had lying around, then retrofitted the directory layout. It would have saved me several frustrating syncs to design the apps/+workloads/ split upfront.

Don’t skip health checks. ArgoCD has good default health checks for standard Kubernetes resources, but custom CRDs need custom health check scripts. I spent time wondering why a Kustomization from Flux (leftover from an experiment) showed as Healthy when it wasn’t — ArgoCD had no idea how to check it. If you’re managing CRDs, define health checks for them.

Prune with caution at first. prune: true is powerful and will delete things. Make sure your Git state is actually authoritative before enabling it. I once accidentally pruned a deployment I thought was in Git but wasn’t. It came back up in seconds, but it was a good reminder to verify before trusting.

Watch the sync wave ordering for CRDs. If you’re deploying operators and their CRDs in the same sync, the CRDs need to apply before the custom resources that use them. Without sync waves, ArgoCD applies everything in parallel and you get no matches for kind errors. Wave 0 for CRDs, wave 1 for the operator deployment, wave 2 for custom resources.

Use the UI for debugging, Git for everything else. The ArgoCD UI is genuinely good for understanding what’s drifted and why. But the source of truth is always Git. I make the change in Git, let ArgoCD sync it, and only use the UI to observe — never to make direct changes.

App-of-apps isn’t complicated, but it requires committing to the discipline: everything goes through Git, every change is a commit, every rollback is a revert. Once that click happens, the homelab stops feeling like an untracked collection of running things and starts feeling like a system you actually understand. That’s worth the initial setup investment.

Context Engineering vs Prompt Engineering: the shift in how we build AI systems.

You receive a pull request coming from an AI. The code looks clean, follows the prompt, and all unit tests pass. Then you notice it uses a library the team deprecated last quarter. And a design pattern that violates the service’s architecture document. The code looks right, but it is completely wrong in the context of your system. This is the absolute limit of prompt engineering. The whole “context engineering vs. prompt engineering debate” comes down to a practical problem: our AI tools keep delivering code that requires careful manual fixes.

Prompt engineering works well for isolated tasks. Building software is not a sequence of isolated tasks. It is a chain of decisions constrained by existing code, team habits, and business rules. The problem is not a poorly written prompt. The problem is that the model has no idea what is happening outside its small window. Asking a better question does not help when the model cannot see the rest of the codebase.

The costs of prompt engineering at scale

Using prompt engineering for anything larger creates a fragile, high-maintenance system. You get stuck in a loop of tweaking prompts to fill gaps in the model’s knowledge, and then everything breaks when the model gets updated or the problem becomes harder.

A maintenance nightmare

When an AI system depends only on the prompt, the system becomes hard to maintain and easy to break. Engineers write absurdly complex prompts to get the right behavior, and those prompts turn into a mess that is impossible to maintain, full of assumptions about how the model “thinks”.

This fails in a few ways. A small change in the output means you have to go hunting for which prompt to modify, which is even worse when prompts are chained. When a provider releases a new model version, your carefully tuned prompts can break. An instruction that worked perfectly yesterday can produce a completely different format or a logical error today. And when something fails, it is hard to debug. You end up guessing whether the problem was the prompt or the lack of context, because you cannot easily reproduce the exact state the model was in.

You can only optimize a prompt so far

There is a limit to what you can achieve by only refining a text prompt. Building software requires awareness, not just a good instruction. A prompt cannot contain a project’s dependency graph, the reasoning behind an old architectural decision, or how the team prefers to handle asynchronous operations.

You end up with code that is technically correct, but in practice is useless. Error handling is a perfect example. A model will generate generic `try/catch` blocks because it knows nothing about structured logging, error types, or the system’s metrics patterns. The code works, but it is incomplete and does not fit, which means a developer will need to fix it. Without system awareness, AI produces unpredictable results that make people lose trust in the tool.

Context Engineering vs. Prompt Engineering: a different way to think

We need to stop trying to cram everything into the prompt. We should focus on designing the environment the model works in, not just optimizing the instruction. This means building systems to provide the model with the specific and explicit information it needs to make decisions that actually fit.

Context engineering is the work of designing, building, and maintaining the systems that collect, filter, and provide this information to the model.

More than just a prompt

An interaction with an LLM should not be `prompt -> output`. It should be `(context + prompt) -> output`. The prompt is just a small part of a much larger package. This operational context can include data, like relevant code from other files, database schemas, or API contracts. It can also include tools, which are functions the model can call to get more information (like running a linter or checking user permissions). It can even include state, like which files the user has open or which commands they just ran.

This completely changes the work. We are architecting an AI environment, not just writing prompts. The real work is deciding which information needs to be explicitly provided and what the model can be expected to know. A code style guide is explicit context. Python syntax is implicit knowledge.

Context engineering is system design

This approach changes how we build with AI. Instead of endlessly tweaking text, we move to building structured and predictable components. A context engineer thinks about the whole system, while a prompt engineer focuses on a single interaction.

A prompt engineer tries to improve a response. They might change a prompt from “write a function that does X” to “acting as a senior software engineer, write a pure function that does X, follow functional programming principles, and include property-based tests.” This might give you a better answer in that specific case.

A context engineer wants to make the entire system more consistent and reliable. They build the infrastructure that automatically finds the team’s functional programming principles in the wiki, pulls examples of existing property-based tests from the code, and provides all of that as context. The prompt can stay simple. The system becomes more reliable because the model’s decisions are now based on real project data.

The real intelligence is not in the model. It is in the system that feeds it. Building AI products that are sustainable depends on this.

Principles for building with context engineering

This requires a more disciplined way of passing information to models. It looks a lot like system design, which is a good sign.

Design your system to deliver context

The first step is to treat context as a core part of your system architecture. The code that fetches the database schema should not be mixed with the code that formats the prompt. You should create separate modules or services that only provide specific pieces of context. This makes the system much easier to test and maintain.

Whenever possible, provide context as JSON or YAML, not just as a large block of text. This helps the model interpret the information more reliably. For example, provide the style guide as a JSON object of lint rules instead of pasting raw text.

You also need versioning for your context. API schemas change, documents get updated. Your system should be able to point to specific versions of these sources. This is the only way to reliably reproduce a past generation for debugging.

Thinking in layers of context

It helps to think of context as a stack of layers. Each layer provides a different type of information. This helps prioritize and filter what you send to the model, which matters for staying within token limits and avoiding noise.

A context stack for a coding task could look like this:

  • Layer 0 (Global): The model’s built-in knowledge of a programming language.
  • Layer 1 (Organization): Company engineering standards or preferred libraries.
  • Layer 2 (Project): Architecture patterns for this project, lint rules, and dependency list.
  • Layer 3 (Local): The content of the current file and other related files the user has open.
  • Layer 4 (Dynamic): Real-time feedback from a compiler or test runner.

A feedback loop is already embedded in this idea. If the model generates code that fails a lint check, that failure becomes dynamic context for the next attempt. The system can self-correct using immediate and factual feedback.

Applying context engineering in practice

To make this work, you need to build the infrastructure to manage context as code.

Managing context with code

Orchestration tools can help, but a well-defined set of functions or microservices also works. The point is to have a programmable way to assemble and provide context. An orchestrator can call different context providers depending on the user’s request, assemble the final package of information, and send it to the model.

You also need to validate and monitor your context. Before sending information to the model, check it. Does the API schema parse? Does the file path exist? Keep an eye on the quality of your context sources. Outdated or incorrect context is worse than no context.

Finally, the system needs to adapt the context to the task. A request to refactor a function needs different information than a request to write a new database migration. Your code needs to be smart enough to fetch the right context for each type of work.

A checklist for building with context

When you are designing a new AI feature, ask these questions:

  • What information is needed? Identify every piece of information the model needs to make a good decision (source code, docs, schemas, git history, team patterns).
  • What is the scope? For a specific task, what is the boundary of the context? The current file, the package, the entire repository? How do you define this in code?
  • How do we know the context is good? How do you ensure the context is correct and up to date? What is the fallback if a source is unavailable?
  • How does it stay updated? When and how is the context updated? On every request, on a time interval, or after an event like a git commit?
  • Can we debug it? Are you logging exactly which versioned context was sent to the model along with the prompt? Can you perfectly reproduce a past generation

Answering these questions shifts the work from guessing the magic words in a prompt to building a software system that is reliable and easy to debug. Small improvements in a prompt lead to small improvements in the output. Improving the context changes the reliability of the entire system.

The Day I Realized It Wasn’t Just About Skills

This is a submission for the 2026 WeCoded Challenge: Echoes of Experience

I used to believe something very simple.

If I learn enough, build enough projects, and keep improving…

I’ll get my place in tech.

That’s what we’re told, right?

Work hard. Stay consistent. Results will come.

But somewhere along the way, I noticed something that didn’t sit right.

Not Everyone Starts at the Same Line

In college, I saw people who were incredibly talented —

people who could build things I couldn’t even understand at that time.

And still… they hesitated.

Not because they lacked skills.

But because they lacked confidence to be seen.

Some thought:

  • “My English isn’t that good.”
  • “I’m not from a big college.”
  • “People like me don’t usually get these opportunities.”

And honestly… I’ve felt that too.

There were moments I questioned:

“Am I really good enough, or am I just trying?”

The Quiet Bias We Don’t Talk About

Nobody says it openly, but it’s there.

Sometimes people are judged before they even speak.

By their:

  • Name
  • Accent
  • Background
  • College

And the worst part?

You start judging yourself the same way.

You hold back.

You don’t apply.

You don’t share your work.

Not because you can’t…

But because you feel like you don’t belong.


The Thought That Changed Everything

One night, while thinking about all this, a simple idea came to me:

What if people saw your work before they saw you?

No name.

No background.

No labels.

Just:

  • What you’ve built
  • What you’ve learned
  • What you’ve overcome

Just your story and your skills.

That’s How “The Invisible Resume” Was Born

I imagined a platform where:

  • You don’t enter your name first
  • You don’t show your college first
  • You don’t get filtered before being understood

Instead, you show:

  • Your projects
  • Your journey
  • Your growth

And only after someone connects with your work…

they get to see you.

Why This Matters

Because talent is everywhere.

But visibility is not.

Because sometimes, the hardest part is not learning…

it’s being seen.

And maybe, just maybe,

if we delay identity for a moment…

We give people a fair chance.

A Small Hope

This isn’t just an idea for a project.

It’s a reminder.

That behind every profile…

there’s a story.

And behind every story…

there’s someone who just wants a chance.

Final Thought

This is not about hiding identity.

It’s about letting talent speak before bias begins.

If you’ve ever felt unseen in tech,

this is for you.

And if you’ve never noticed this before,

maybe now you will.

AI-Powered Data Science Team for Accelerated Task Completion

Empowering Data Science with an AI-Powered Agent Team: A 10X Speed Boost

We’re thrilled to introduce an innovative open-source project: an AI-powered team of agents designed to dramatically accelerate common data science tasks. This initiative aims to equip the developer and data science community with tools that can deliver up to 10X faster performance.

What is the AI Data Science Team?

This is not just another library; it’s a collaborative framework where AI agents work together to perform complex data science operations. From data preprocessing to model deployment, these agents are engineered to streamline the entire workflow. We envision a future where AI significantly augments human capabilities in data science, making advanced analytics accessible to a wider audience.

Key Benefits:

  • Unprecedented Speed: Accomplish tasks in a fraction of the time.
  • Intelligent Automation: Reduce manual effort and minimize errors.
  • Collaborative Environment: Foster innovation and knowledge sharing within the #BuilderCommunity.
  • Open-Source & Accessible: Empowering everyone to leverage cutting-edge AI.

Who is this for?

  • Data Scientists seeking to optimize their workflow.
  • Researchers looking for faster data analysis.
  • Developers aiming to integrate advanced AI capabilities into their applications.
  • Anyone passionate about the intersection of AI and data science.

Get Involved!

This project is a testament to the power of open-source collaboration. We invite you to explore the codebase, experiment with the agents, and contribute your expertise. Your feedback and contributions are vital in shaping the future of AI in data science.

Join us in building a more efficient and innovative data science ecosystem.

Stelixx #StelixxInsights #IdeaToImpact #AI #DataScienceTeam

Stop Writing postMessage Manually For Workers — I Built a Decorator for That

Stop Writing postMessage Manually For Workers — I Built a Decorator for That

Tags: angular, react, webdev, javascript

Web Workers are one of the most underused features in modern web development. They let you run heavy JavaScript off the main thread — keeping your UI smooth and responsive.

But the API is painful:

// Standard Worker code — just to call ONE function
const worker = new Worker('./my.worker.js');
const requestId = Math.random();

worker.postMessage({ id: requestId, command: 'processData', payload: data });

worker.onmessage = (event) => {
  if (event.data.id === requestId) {
    console.log(event.data.result);
  }
};

You need request IDs, response matching, manual routing, port management for SharedWorkers… for every single method call.

So I built ngx-worker-bridge to eliminate all of that.

What it looks like instead

Define your background logic (runs in the worker thread):

import { setState } from 'ngx-worker-bridge';

export class DataModule {
  private count = 0;

  increment() {
    this.count++;
    setState('counter', this.count); // broadcasts to ALL tabs
  }

  async processData(payload: any) {
    // heavy work here — UI thread never blocks
    return expensiveOperation(payload);
  }
}

Call it from Angular like a normal service:

import { Injectable } from '@angular/core';
import { RunInWorker, workerStore } from 'ngx-worker-bridge';

@Injectable({ providedIn: 'root' })
export class DataService {
  count$ = workerStore<number>('counter', 'shared'); // reactive, auto-updates

  @RunInWorker({ bridge: 'shared', namespace: 'data' })
  increment(): Promise<void> { return null as any; }

  @RunInWorker({ namespace: 'data' })
  processData(payload: any): Promise<any> { return null as any; }
}

Or from React with a hook:

import { useWorkerStore } from 'ngx-worker-bridge/react';

function App() {
  const count = useWorkerStore<number>('counter', 'shared');
  return <button onClick={() => service.increment()}>Count: {count}</button>;
}

No postMessage. No onmessage. No request ID tracking. Just a decorator and a hook.

The SharedWorker feature

The real power comes from SharedWorker support. A SharedWorker runs once and is shared across all open browser tabs. When you call setState(‘counter’, value) inside your module, it broadcasts to every connected tab instantly.

Open 5 tabs — they all stay in sync. Without a server. Without WebSockets.

This is useful for:

  • Notification counts
  • Live stock/crypto prices
  • Shared timers or sessions
  • One WebSocket connection shared across all tabs

Install

# Angular (RxJS already included)
npm i ngx-worker-bridge

# React
npm i ngx-worker-bridge rxjs

Would love feedback — especially from anyone who’s dealt with SharedWorker pain before.

Introduction to Java Exception Handling

What is Exception Handling?

Exception Handling in Java is a mechanism to handle runtime errors so that the normal flow of the program can be maintained.

An exception is an unwanted or unexpected event that occurs during program execution and disrupts the normal flow of instructions.

Why Exception Handling is Important?

  • Prevents program from crashing
  • Maintains normal flow of application
  • Helps in debugging errors easily

Basic Keywords in Exception Handling

  • try → block where code is written
  • catch → handles the exception

simple Example Program

public class ExceptionExample {
    public static void main(String[] args) {

        try {
            int a = 10;
            int b = 0;

            int result = a / b; // Exception occurs here

            System.out.println(result);
        } 
        catch (ArithmeticException e) {
            System.out.println("Cannot divide by zero");
        }
    }
}

CVE-2026-33044: CVE-2026-33044: Stored Cross-Site Scripting in Home Assistant Map-Card

CVE-2026-33044: Stored Cross-Site Scripting in Home Assistant Map-Card

Vulnerability ID: CVE-2026-33044
CVSS Score: 7.3
Published: 2026-03-27

Home Assistant versions prior to 2026.01 are vulnerable to a stored Cross-Site Scripting (XSS) flaw in the Map-card component. An authenticated attacker can inject malicious JavaScript into an entity name, which executes when a victim hovers over historical movement data points in the dashboard.

TL;DR

A stored XSS vulnerability in the Home Assistant Map-card allows authenticated attackers to execute arbitrary JavaScript in a victim’s browser context by injecting HTML payloads into device entity names.

⚠️ Exploit Status: POC

Technical Details

  • CVE ID: CVE-2026-33044
  • CWE ID: CWE-79
  • Attack Vector: Network
  • CVSS 4.0 Score: 7.3
  • Impact: Account Takeover / Session Hijacking
  • Exploit Status: PoC Available
  • CISA KEV Status: Not Listed

Affected Systems

  • Home Assistant Core
  • Home Assistant Frontend
  • homeassistant: >= 2020.02, < 2026.01 (Fixed in: 2026.01)

Exploit Details

Mitigation Strategies

  • Upgrade Home Assistant to version 2026.01.
  • Disable the hours_to_show property in Map-card configurations until patched.
  • Enforce strict HTML output encoding for all user-controlled data in frontend components.
  • Audit existing entity names for unauthorized modifications or HTML payloads.

Remediation Steps:

  1. Access the Home Assistant administrative interface.
  2. Navigate to Settings > System > Updates.
  3. Identify the pending update for Home Assistant Core version 2026.01.
  4. Initiate the backup process to secure the current configuration.
  5. Apply the update and monitor the system logs during the restart process.
  6. Verify the application version displays 2026.01 in the ‘About’ section.

References

Read the full report for CVE-2026-33044 on our website for more details including interactive diagrams and full exploit analysis.

Technical Debt: When to Fix, When to Ship

Every engineering team carries debt. The question is never whether you have it. The question is whether you understand it well enough to make deliberate decisions about it, or whether you are just hoping it does not become a crisis before you get around to dealing with it.

Most teams are in the second camp. Not because the engineers do not care, and not because the managers are incompetent, but because technical debt is genuinely hard to reason about. It is invisible to most stakeholders. It compounds quietly. Its costs show up as friction and slowness rather than as clean line items on a budget. And the tradeoff between addressing it now versus shipping something now is almost always under time pressure, which means the default is almost always to ship.

I want to give you a framework for thinking about debt more deliberately – one that helps you decide when fixing is the right call, when shipping is the right call, and how to communicate either decision to the people who care about outcomes rather than architecture.

Before we get into the framework, it is worth being precise about what technical debt actually is, because the term gets used loosely in ways that muddle the decision-making.

Ward Cunningham’s original metaphor was specific: technical debt is the extra work created when you take a shortcut to ship faster, with the understanding that you will come back and do it properly later. The key word is deliberate. You knew it was a shortcut. You made a conscious tradeoff. That is very different from code that is just poorly written because someone did not know better, or a design that seemed correct at the time but was invalidated by requirements that could not have been anticipated.

In practice, most engineering teams use “technical debt” to describe all three of those things, which is fine for casual conversation but creates confusion when you are trying to prioritise. Deliberate shortcuts have a specific remorse profile – you know what you did, you know roughly what fixing it would take, and you can reason about when the tradeoff tips toward fixing. Legacy code that was written under different assumptions, or architectural decisions that made sense at a previous scale, are harder to reason about because the original context is often lost and the cost of addressing them is harder to estimate.

For the purposes of decision-making, the useful distinction is not between types of debt by origin but between debt by impact. Specifically: is this debt actively costing you velocity right now, or is it a latent risk that has not yet materially affected your ability to work?

High-impact debt – the kind that is actively slowing the team down, generating frequent bugs, making changes in a certain area disproportionately risky, or creating cognitive overhead every time someone has to work near it – that is debt with a measurable present cost. You can point to it in sprint data: this area of the codebase takes three times as long to change as comparable areas, and it accounts for a disproportionate share of production incidents.

Latent debt – the kind that is messy and uncomfortable but not yet materially impacting delivery – is real, but it has a different urgency profile. Addressing it might still be the right call for other reasons, but it is harder to justify against immediate delivery needs without a clear and specific articulation of the risk.

The framework I use for debt prioritisation has three dimensions: velocity impact, risk profile, and strategic alignment.

Velocity impact is the question of whether the debt is actually costing you delivery speed right now. If you can measure it – and often you can, in cycle time data, bug rates by subsystem, or engineer time estimates on adjacent work – use the numbers. “This service generates forty percent of our incidents but represents ten percent of our codebase” is a compelling velocity impact argument. “This code is messy and would be nicer if it were cleaner” is not.

Risk profile is the question of what happens if the debt is not addressed. Some debt sits in a part of the system that is unlikely to change significantly – it is messy but it is also relatively stable and not under active development. That debt has a low risk profile even if it is aesthetically uncomfortable. Other debt sits in a critical path that is about to receive significant investment, or in a part of the system where a failure would be disproportionately damaging. That debt has a high risk profile even if it is not currently causing visible problems.

Strategic alignment is the question of whether the work that would fix this debt is work that matters for where the product is going anyway. Sometimes the most efficient path is to address debt as part of a larger piece of work that is already planned – you are rebuilding the payments flow anyway, so cleaning up the debt in the payment service is low incremental cost. Sometimes the debt is in a part of the system that is likely to be deprecated or replaced entirely, in which case investing in it now is a waste.

When you look at a piece of debt through all three of those lenses, the decision often becomes cleaner. High velocity impact, high risk profile, and in a strategically important area: address it, and address it soon. Low velocity impact, low risk profile, in an area the product is moving away from: leave it, and stop feeling guilty about it.

The harder cases are the mixed ones – debt with moderate velocity impact and moderate risk, competing with genuine product priorities for engineering time. This is where the “fix it in pieces” approach often makes sense. Not a dedicated debt sprint that product stakeholders will resent and that rarely fully succeeds anyway, but a standing allocation of capacity toward high-priority debt items worked into every sprint. Ten to twenty percent is the range I see working in practice. Enough that meaningful progress gets made, not so much that it creates constant friction with delivery commitments.

There is a case against the dedicated debt sprint that is worth making explicitly because the instinct to batch debt work into a single concentrated effort is very common and very understandable. The problem is that it creates a boom-bust cycle. You accumulate debt under delivery pressure, you hit a tipping point, you negotiate a debt sprint, you clean up the worst of it, and then you go back to accumulating. The underlying rate of accumulation does not change because the sprint did not change the culture or the incentives – it just cleared the queue.

A standing allocation changes the culture more durably because it normalises debt management as a continuous practice rather than an emergency response. It also keeps engineers closer to the debt, which means they are better positioned to identify which parts of it are actually costing velocity and which are just aesthetically uncomfortable. That distinction matters a lot for prioritisation.

Now let’s talk about stakeholder communication, because this is where a lot of technically strong engineering leaders stumble.

The engineers on your team understand why debt matters. They live with it. They feel it every time they work in a slow, fragile, or confusing part of the codebase. But the product managers, business stakeholders, and executives you need to align with do not feel that friction, and they are not going to be persuaded by architectural arguments. They are going to be persuaded by arguments about outcomes.

That means translating the debt conversation into the language of risk and velocity. Not “we have a lot of legacy code in the payment service” but “our payment service currently takes our engineers three times as long to change as comparable services, and it generates more than a third of our production incidents. That is costing us roughly two sprint cycles per quarter in incident response and rework, and it is the main reason we keep missing our estimated delivery dates on anything that touches payments.”

That framing gives the stakeholder something to weigh. It turns a vague technical discomfort into a specific cost, and it makes the tradeoff legible: we can invest capacity here and expect these benefits over this timeframe, or we can continue deferring and expect to keep paying this ongoing cost.

Be honest about uncertainty in those estimates. If you are saying it costs two sprint cycles per quarter, that should be a real estimate based on real data, not a number you made up to make the argument more compelling. Stakeholders who get burned by overconfident technical estimates stop trusting technical estimates, which makes every subsequent conversation harder.

The inverse is also true for shipping decisions. When the right call is to ship with known debt rather than delay to fix it, say so explicitly and document it. “We are shipping this with a known shortcut in the session handling code. It is fine for our current traffic levels but will need to be addressed before we scale past X. Estimated cost to address: one engineer week. Suggested timeline: before Q3 scaling work.” That kind of explicit acknowledgment does two things: it keeps the debt visible rather than letting it quietly become background noise, and it demonstrates the deliberate reasoning that builds trust with stakeholders that you are managing these decisions thoughtfully rather than just letting things slide.

The most important habit you can build around technical debt is measuring it. Not in some comprehensive, difficult-to-maintain debt registry, but in the practical proxy metrics that tell you whether it is getting better or worse over time. Cycle time by area of the codebase. Incident frequency by service. Change failure rate. Time to onboard new engineers to different parts of the system. These are imperfect proxies but they are real data, and they give you something to point to when the debt conversation gets abstract.

They also give you a way to demonstrate progress. “The work we did on the payment service over the last two quarters has reduced incident frequency in that area by sixty percent and cut the average cycle time for payment changes from four days to one and a half” is a compelling narrative. It makes the case for ongoing investment in debt reduction more credibly than any theoretical framework could, because it shows that the investment actually worked.

Technical debt is not a failure of engineering discipline. Every team that has ever shipped software under real constraints has it. The teams that manage it well are not the ones that have less of it – they are the ones that are honest about it, deliberate about prioritising it, and fluent enough in the business language to communicate about it in terms that the people making resource decisions can actually use.

The goal is not a debt-free codebase. The goal is a codebase where the debt you carry is debt you chose, debt you understand, and debt you are managing toward a specific outcome. That is a much more achievable and much more useful standard.

Next in the series: Leading Through Uncertainty – decision-making under pressure, communication cadence, and maintaining team morale when the path forward is not clear.

Scalable Design of Agent

Functional Requirements

  1. Understand the intent from the user’s conversation.
  2. Break the intent into a series of steps needed to achieve it.
  3. Build an execution path for those steps, including ordering and dependencies.
  4. Enforce authentication and authorisation before any agent logic runs. Every request must pass identity verification (JWT / OAuth2), role-based access control (RBAC), and session binding so the authenticated identity carries across all downstream calls. Unauthenticated requests are rejected at this layer.
  5. Human in the loop (HITL) before execution. The user must be able to review the plan, request changes, or reject it before the agent acts.
  6. Confirm the task operation flow via chain-of-thought reasoning. Once the plan is approved, break it into tasks and validate the sequence with the user. (TODO: Research reasoning models — distinguish the model’s internal CoT from the agent’s explicit task graph, and how to surface steps for HITL review.)
  7. Extract entity models and data from the conversation. Outputs must conform to canonical schemas (Pydantic or Zod) so extraction is consistent across turns and execution plans are reliable.
  8. Execute the confirmed task plan and report completion to the user.

Non-Functional Requirements

  1. Async IO for all LLM calls, with exponential backoff on transient failures.
  2. HITL looping. Support iterative review cycles, including the ability to reject a plan and return to the planning stage. (TODO: paste Claude diagram reference.)
  3. Parallel and series task detection. The task router identifies independent tasks (fan out in parallel) and dependent tasks (run in sequence).
  4. Active context window management. If the conversation risks hitting the context limit (TODO: confirm — 128k or higher depending on model), use subagents. Before evicting content, summarise the evicted portion and inject it as a system message so no history or entity state is lost.
  5. Sliding window as an alternative. Use a sliding window when task state is small and linear; use subagents when tasks are independent and parallelisable. Evaluate both before committing.
  6. Fan-out and pub/sub for scale. The async executor fans out to parallel tasks and uses pub/sub for event-driven coordination.
  7. Explicit failure handling. Exhausted retries go to a dead-letter queue (DLQ). Timeout policies are defined per task type. Repeated failures escalate to HITL rather than dropping silently.
  8. Prompt injection protection. The security layer must cover:
  9. Input sanitisation: escape user content before injecting into prompts.
  10. Privilege separation: user input never reaches the system prompt directly.
  11. Output validation: all LLM outputs are untrusted until validated, including tool-call responses.
  12. Guard model classifier: a fast pre-check on inputs before the main agent runs.
  13. Prompt versioning and promotion. Use a three-stage model: dev → staging → prod. Each stage has its own eval suite. Promotion requires passing evals at the current stage, giving a clear rollback path for any regression.
  14. Observability from day one. Every LLM call emits structured traces (OpenTelemetry or equivalent) covering input, output, latency, model, and token count. Retrofitting tracing later breaks the eval feedback loop.
  15. Multi-turn session identity. Conversation history, entity state, and the task graph are tied to a persistent user identity so the agent can resume interrupted intents across sessions.

Scope: Stretch

How to detect when the agent has underperformed in production, and feed that back into development:

  1. Eval suite per environment. Dev, staging, and prod each have an eval suite. Prod traces are continuously scored to catch regressions early.
  2. Failure detection. The guard model classifier and output validator flag suspect interactions for human review.
  3. Prod → dev feedback loop. Flagged interactions become new eval cases in dev. Prompt fixes are iterated in dev, pass staging evals, and are promoted via the versioned prompt registry.
  4. Reasoning model research. Investigate how reasoning models handle internal chain-of-thought versus the agent’s explicit task graph, and how to surface steps for HITL review.
  5. Entity schema layer. Canonical Pydantic / Zod models for all entities extracted from conversation.
  6. Context summarisation. Before evicting window content, summarise the block and re-inject as a compressed system message to preserve intent fidelity over long conversations.