.NET AI Architect Laboratory: Making AI Work and Execute Tools (Phase 2)

In Phase 1 of this project, we built a type-safe “Brain” using .NET 10 and Google Vertex AI. In Phase 2, we successfully gave hands and feet to our AI substrate. By connecting Microsoft Semantic Kernel, we created an autonomous agent that can read real local project files, think independently, and detect security vulnerabilities or performance bottlenecks.

Here is a quick summary of what I accomplished and the critical technical lessons I learned during Phase 2.

🛠️ Key Accomplishments

Autonomous Tool Execution: The AI agent now receives a high-level goal (e.g., “Find architectural patterns and security bugs”). It automatically runs native C# plugins like ListProjectFiles and ReadCodeFile to explore and analyze the workspace without manual guidance.

Dynamic Provider Routing: I implemented a dynamic runtime pipeline. The system can switch between Gemini 2.5 Flash and Groq (Llama 3.3 / Llama 3.1) on the fly based on user selection, completely decoupling the business logic from a single AI vendor.

Angular 19 Cyber-Dashboard: Built a modern interface using Angular Signals and Tailwind CSS v3 to visualize the agent’s real-time analysis results, complexity scores, and strategic recommendation logs.

🧠 Important Technical Lessons Learned

The .NET Scope Trap: Trying to resolve request-scoped Semantic Kernel instances directly from the root IServiceProvider container crashes the Kestrel server. I resolved this runtime boundary issue by shifting keyed service lookups to HttpContext.RequestServices to keep the kernel container safely isolated per HTTP request.

Enforcing Deterministic JSON: Small models like Llama 3.1 8B love to output conversational text or messy “thought chains” instead of clean data. Prompt engineering alone is too brittle. I fixed this by forcing strict ChatResponseFormat.Json settings at the API gateway level to mechanically guarantee valid JSON parsing.

The Token Bleeding Filter: Forcing an LLM to output rigid JSON payloads while explaining complex source code in Turkish can confuse the token selection matrix, leaking weird characters (like Russian or Chinese letters). Instead of wasting prompt tokens, I added a simple Whitelist Regex Sanitizer inside the client-side Angular signal flow to clean them up instantly.

🖥️ Dashboard Interface Preview

Here is how the live cyber-panel looks when a Llama-driven autonomous agent evaluates local C# source code:

🚀 What’s Next? (Phase 3: Enterprise Memory)

Now that our type-safe brain can execute tools and display results perfectly, the next challenge is context retention. Fitting massive codebases into a single prompt window is expensive and degrades model accuracy.

Next, we start Phase 3: Enterprise Memory (RAG). I will integrate PostgreSQL/pgvector into the gateway to give our agent a permanent memory substrate of project histories and engineering standards.

🔗 Explore the Code

The complete blueprint, architecture blueprints, and step-by-step implementation files for Phase 2 are fully open-source. Feel free to explore, clone, or follow along as the laboratory evolves:

👉 GitHub Repository: github.com/muratsuzen/dotnet-ai-lab

How to Document APIs in Confluence

How to Document APIs in Confluence

API docs fail for the same reasons every time: they live in too many places, nobody owns them, and updating them feels optional.

The Postman collection is the real reference. The Swagger file is six months behind the code. The Confluence page was accurate once. The README has a curl example that stopped working in v2. Developers new to the codebase have to triangulate between all of them and ask a colleague anyway.

Centralizing API documentation in Confluence doesn’t fix all of this automatically — but it gives you a single place to link from everywhere else, and the right structure makes it far easier to keep up to date.

Why API Documentation Gets Stale

The core problem isn’t that people are lazy. It’s that documentation lives in a different tool than the code, so every change requires two actions: update the code, then update the docs. That second step gets skipped when deadlines are tight, and it never catches up.

Secondary problems layer on top:

  • No clear owner. The developer who built it moves on. Nobody else knows enough to update it confidently.
  • Wrong format. Prose descriptions of request bodies are ambiguous. Code examples with no explanation assume too much. A mix of both with no consistent structure is the worst of both worlds.
  • Swagger/OpenAPI as the only source. Auto-generated docs show you what the API does, not why, what the gotchas are, or what the actual production behavior looks like in edge cases.

Good API documentation combines auto-generated spec output with hand-written context. Confluence is where the hand-written context should live.

What Good API Documentation Includes

For each API (or logical group of endpoints), your documentation should cover:

Authentication and authorization — how to get credentials, token lifetime, scope requirements, which endpoints need which permissions. This is the most-asked question from new integrators and the hardest to find in auto-generated docs.

Base URL and versioning — current version, how versioning works, deprecation policy. A table showing v1/v2 differences is worth more than a paragraph.

Endpoint reference — for each endpoint: method, path, description, request parameters (path, query, header, body), example request, example response, error codes. See the template at the bottom of this post.

Error codes — a dedicated table of all error codes, what they mean, and what the caller should do about them. 400 Bad Request is not useful documentation.

Changelog — what changed between versions and when. The changelog is the first thing an existing integrator checks when something breaks after a release.

Rate limits and quotas — numbers, what triggers throttling, what the response looks like, how to handle retry.

Structuring API Docs in Confluence

A Confluence space structure that works well for a team maintaining one or more APIs:

API Documentation (space root)
├── Getting Started
│   ├── Authentication
│   ├── Rate Limits & Quotas
│   └── SDKs & Client Libraries
├── API Reference
│   ├── Users API
│   │   ├── GET /users
│   │   ├── POST /users
│   │   ├── GET /users/{id}
│   │   └── DELETE /users/{id}
│   └── Orders API
│       └── ...
├── Changelog
│   ├── v3.0.0 (2026-04-01)
│   ├── v2.5.0 (2025-12-10)
│   └── ...
└── Guides
    ├── Handling Pagination
    ├── Webhook Setup
    └── Error Handling Patterns

Keep the API Reference section flat within each API group — one page per endpoint. Deep nesting makes the sidebar useless. The Changelog and Guides sections are where you write prose; the Reference section is where you write specs.

Writing Request and Response Examples in Confluence

This is where most Confluence API docs fall apart. People write prose descriptions of JSON fields, or paste unformatted request examples that are impossible to scan.

The right approach is structured code blocks with clear labels. Markdown Renderer for Confluence lets you write these directly in markdown on any Confluence page — including fenced code blocks with language hints that render with syntax highlighting.

A request example written in markdown on a Confluence page:

**Request**

http
POST /v3/orders
Authorization: Bearer {token}
Content-Type: application/json

{
“customer_id”: “cus_8f3k2”,
“items”: [
{ “sku”: “WIDGET-001”, “quantity”: 2 },
{ “sku”: “GADGET-042”, “quantity”: 1 }
],
“shipping_address”: {
“line1”: “123 Main St”,
“city”: “Austin”,
“state”: “TX”,
“zip”: “78701”
}
}
`

Response — 201 Created

json
{
"order_id": "ord_9g4m1",
"status": "pending",
"created_at": "2026-05-20T14:32:00Z",
"total_cents": 4799
}
“


This renders cleanly in Confluence via Markdown Renderer, and it's easy to edit. No Confluence macro hunting, no storage format XML — just write markdown.

---

## Keeping Docs in Sync with Code

The documentation drift problem doesn't go away with better structure — it needs a process fix too.

**Link the Confluence page from the code.** Add a comment in the route handler or controller pointing to the Confluence page. When a developer changes the endpoint, the link is right there.

python

POST /v3/orders

Docs: https://yourspace.atlassian.net/wiki/x/AbCdEf

@app.route(“/v3/orders”, methods=[“POST”])
def create_order():
…


**Add the docs update to your PR checklist.** If your team uses a PR template, add a checkbox: "Updated API docs in Confluence if endpoints changed." It sounds obvious but it works.

**Use the Confluence REST API for automated updates.** For teams with documentation-as-code workflows, the Confluence REST API lets you push page updates from CI. Your OpenAPI spec can be auto-rendered into a Confluence page on every merge to main, and the hand-written context pages stay separate and stable.

bash

Update a Confluence page via REST API

curl -X PUT
“https://yoursite.atlassian.net/wiki/rest/api/content/{pageId}”
-H “Authorization: Bearer $CONFLUENCE_TOKEN”
-H “Content-Type: application/json”
-d ‘{
“version”: { “number”: 12 },
“title”: “GET /users/{id}”,
“type”: “page”,
“body”: {
“storage”: {
“value”: “

Updated content here

“,
“representation”: “storage”
}
}
}’


---

## Page Template: API Endpoint Reference

Here's a practical template for a single endpoint page. Copy it, fill it in, repeat per endpoint.

markdown

Overview

Brief description of what this endpoint does and when to use it.

Request

Method & Path: POST /v3/users

Authentication: Bearer token required. Scope: users:write

Headers

Header Required Description
Authorization Yes Bearer {token}
Content-Type Yes application/json

Body Parameters

Field Type Required Description
email string Yes User’s email address
name string Yes Display name
role string No One of: admin, member, viewer. Default: member

Example Request

POST /v3/users
Authorization: Bearer eyJhb...
Content-Type: application/json

{
  "email": "[email protected]",
  "name": "Alex Kim",
  "role": "member"
}
` ``

## Response

### 201 Created

json
{
“user_id”: “usr_7c2p9”,
“email”: “[email protected]“,
“name”: “Alex Kim”,
“role”: “member”,
“created_at”: “2026-05-20T10:00:00Z”
}
`

Error Codes

Code Meaning Action
400 Missing required field Check request body against schema
409 Email already registered Use existing account or change email
422 Invalid role value Use one of: admin, member, viewer
429 Rate limit exceeded Back off and retry after Retry-After header

Notes

Any gotchas, rate limit specifics, or behavior that differs from the general rules.
`

Getting Started

Install Markdown Renderer for Confluence from the Atlassian Marketplace to write and edit API docs in markdown directly on Confluence pages — code blocks, tables, and all.

Questions? Reach out via our support portal.

.NET AI Architect Laboratory: My Architectural Experiments and Learning Journey in the AI Ecosystem (Phase 1)

n an era where artificial intelligence technologies are advancing at breakneck speed, the best way to truly grasp new libraries and paradigms is to roll up your sleeves and get into the kitchen. As a software developer, I launched the .NET AI Architect Laboratory project to pull back the curtain on the underlying mechanics of integrating LLMs into our applications, and to explore the absolute boundaries of building flexible, modular architectures.

For me, this laboratory is far from a commercial endeavor; it is a personal learning journey where I discover new technologies, document my findings, and build a sandbox grounded in enterprise-grade architectural standards.

🎯 Why This Lab?

My primary motivation in this project is to look past standard “quick-start” documentation and truly master the engineering philosophy behind AI integration. The core goals I established for this laboratory include:

Understanding the Kitchen: Going beyond basic API calls to deeply comprehend the architectural costs and mechanics of treating LLMs as plug-and-play engines.

Forging My Own Standards: Handcrafting and testing the cleanest, most sustainable design patterns for AI integration within the .NET ecosystem.

Preparing for Tomorrow’s Tech: Moving past being a passive consumer of ready-made libraries, and reaching a level of competency where I can design end-to-end AI data flows, orchestration, and security from scratch.

🧠 How My Learning Journey is Progressing

This process has been an incredibly rewarding, educational, and discovery-filled experience. A few highlights from this journey so far:

Adapting to an Evolving Ecosystem: Experiencing live version upgrades, shifting method names, and breaking changes in highly nascent libraries like Microsoft.Extensions.AI and the newest official vendor SDKs (such as Google.GenAI) has been a phenomenal learning experience.

Marrying the Rigid and the Fluid: I learned how to build a translation bridge between the strict, type-safe world of .NET and the highly fluid, unpredictable nature of generative AI outputs.

Learning through Failure: Encountering role-matching discrepancies, authentication hurdles, and schema alignment issues during development allowed me to gain a much deeper understanding of how these libraries operate under the hood.

🛠️ What I Accomplished in Phase 1

The first phase of the lab focused entirely on building a clean “Brain” architecture, completely insulated from external vendor lock-in:

Enterprise Setup & Security: I built the foundation using a .NET 10 Minimal API. Instead of passing brittle API keys through code, I established enterprise security standards by leveraging Google Vertex AI via IAM and Service Account (vertex-key.json) mechanics through Application Default Credentials (ADC).

Abstraction via the Adapter Pattern: To prevent the API endpoint from tightly coupling with the native Google SDK, I developed a custom adapter (GoogleGenAIChatClient) that implements Microsoft’s standard IChatClient interface.

Type-Safe JSON Management: To ensure that raw AI outputs could be safely and directly deserialized into C# objects (ArchitectureReview), I enforced strict schema controls. By fine-tuning parameters like Temperature (down to 0.2f) and setting the response MIME type, I successfully locked down the model’s output format.

Dynamic Provider Routing: I designed an elegant Dependency Injection (DI) structure capable of switching between different underlying AI engines at runtime simply by modifying a single line in appsettings.json (e.g., SelectedProvider: “Gemini”).

🚀 What’s Next? (Phase 2 and Beyond)

Now that the core infrastructure is completely provider-agnostic, it’s time to test more autonomous workflows within the laboratory:

Giving the AI Hands and Feet (Orchestration): I will integrate Microsoft.SemanticKernel into the project to present native C# functions (Native Plugins) to the model as tools, opening the door to experimenting with autonomous execution loops.

Code Analysis Capability (Function Calling): The first plugin I develop will test the model’s ability to scan real C# source files in the repository and generate autonomous architectural reviews.

UI & Visualization: To visualize the analysis results and track model performance metrics over time, I will build an Architecture Dashboard powered by Angular 19.

Advanced Memory & Agency (RAG & Agents): In later stages, I plan to explore vector database integrations (PostgreSQL/pgvector) and step into multi-agent systems via the Model Context Protocol (MCP).

🔗 Explore the Code

The complete blueprint, architecture blueprints, and step-by-step implementation files for Phase 1 are fully open-source. Feel free to explore, clone, or follow along as the laboratory evolves:

👉 GitHub Repository: github.com/muratsuzen/dotnet-ai-lab

Why tech leaders should track service level objectives (SLOs) in load testing campaigns

When Canal+ needed to guarantee its streaming platform could handle millions of concurrent viewers during a major live football broadcast, the team didn’t simply run a load test and hope for the best.

They ran progressive, iterative load campaigns against explicit performance targets, identified and resolved bottlenecks in caching and licensing APIs, and optimised machine sizing before a single viewer tuned in. The result: zero incidents during the broadcast. Not “fewer incidents than last time.” Zero.

That outcome didn’t come from running harder tests. It came from running smarter ones — anchored to Service Level Objectives that defined, in user-relevant terms, exactly what “good enough” meant before go-live.

For tech leaders, this is the core argument: load testing without SLOs is activity. Load testing with SLOs is governance.

The framework: SLIs, SLOs, SLAs, and error budgets

Before getting into practice, the terminology needs to be precise — because sloppy definitions lead to sloppy governance.

Google’s SRE literature provides the clearest foundation:

  • SLI (Service Level Indicator): A quantitative measure of service behaviour — request latency, error rate, throughput, availability.
  • SLO (Service Level Objective): The target or acceptable range for that SLI. For example: “99.9% of checkout requests complete within 300 ms over a 30-day window.”
  • SLA (Service Level Agreement): The external commitment to customers, usually with financial penalties attached.
  • Error budget: The allowable unreliability implied by the SLO. At 99.9%, that’s roughly 43 minutes of downtime per month. At 99.99%, it drops to about 4 minutes.
  • Burn rate: How quickly that budget is being consumed, the key signal for operational urgency.

One leadership principle follows immediately from this structure: your internal SLO should be stricter than your public SLA. Google Cloud’s own guidance illustrates this with a 99.95% internal SLO paired with a 99.9% SLA. That gap is a deliberate safety buffer — and running load tests against the internal SLO means you surface contractual risk while there’s still time to fix it.

The second principle is equally important: SLOs must be user-centred, not infrastructure-centred. A load test that only reports CPU utilisation and median response time is measuring what’s convenient, not what customers experience. The right SLI is the one that, if barely met, still keeps the typical user satisfied.

Read more: SLO vs SLA vs SLI: what’s the difference and why It matters

How SLOs change the design of load tests

Most load testing today still asks the wrong question: “What was the maximum RPS we achieved in the lab?” SLO-driven load testing asks a more useful set of questions:

  • At what request rate do we stop meeting the user-relevant objective?
  • How quickly are we burning error budget when we miss it?
  • What component saturates first and how does the system behave when it does?

That reframing has four concrete effects on how campaigns are designed.

  1. Pass/fail becomes explicit: A load test without SLOs may report that p95 latency was 280 ms and CPU reached 78%, but it doesn’t answer whether the system is ready to release. Tools like k6, Gatling, and Azure Load Testing all support encoding user-relevant thresholds directly in test execution, producing a true pass/fail signal rather than a dashboard someone must interpret later.**

    2. Load shapes become more realistic.** Google Cloud explicitly recommends open-loop load patterns for this reason: production clients don’t self-throttle the way closed-loop generators do. Open-loop tests send requests at a steady rate regardless of response times, which better mimics real traffic. A test that passes under artificially polite load can still fail catastrophically when production traffic arrives without courtesy.**

    3. Overload behaviour becomes a first-class objective.** SLO-driven testing doesn’t just ask “what’s our capacity?” It asks “what happens when we exceed it?” Does the system shed load cleanly? Does it recover without cascading failures? These are the questions that matter on launch days and during demand spikes — and they’re the questions that “peak RPS in the lab” benchmarks never answer.**

    4. Short tests connect to long-horizon budgets.** A production SLO is measured over days or weeks; a load test runs for minutes or hours. The bridge is burn rate: you don’t need to recreate an entire month to show that current error rates would exhaust your monthly budget unacceptably fast. That calculation turns a single test run into a release signal.

Try the SLO advisor

The technical upside: five benefits engineers should know

Realistic target-setting

‍SLOs prevent teams from optimising for the wrong number. Lab-only peak throughput figures are internally satisfying but commercially irrelevant. The SLO focuses attention on the tail latency and success rate of the journeys customers actually take.‍

Better prioritization

‍Google’s error-budget policy explicitly uses budget consumption to redirect effort from features to reliability. When a load test shows your checkout service is burning budget at 3× the sustainable rate, that’s a data-driven argument for investing in caching or query optimisation, not a matter of opinion.

Stronger root-cause analysis

‍When a latency SLO fails during a test, the investigation has a starting point: which resource, dependency, or code path saturated first? Correlating load test output with traces, logs, and server-side metrics compresses the time between “something’s wrong” and “here’s why.”

Protection from average-only blindness

‍Google’s “Tail at Scale” research shows why large systems are dominated by latency tails as scale and utilisation increase. The Home Depot’s SLO programme explicitly chose percentile latency over arithmetic averages for exactly this reason. If your release gates use averages while your users feel the p99, you’re under-measuring risk.

Automation and repeatability

‍SLOs, code-based assertions in Gatling make performance testing suitable for CI/CD in the same way unit tests are. For instance, LoginRadius moved away from a JMeter-based approach that wasn’t integrated into its pipeline, and reported latency dropping from 500 ms to 250 ms alongside an 80%+ reduction in production issues.

The business case: five benefits leaders should own

Customer experience protection

SLOs formalise what “acceptable” means in terms customers feel, not in terms that are easy to instrument. Every load test run against an SLO is a forward-looking commitment to that experience under pressure.

SLA risk reduction

‍If a service can’t pass its internal SLO under expected peak conditions, the risk of breaching its public SLA in production is already real — with 54% of significant outages costing over $100,000. Load testing against the internal SLO functions as an early-warning system for commercial exposure — before it becomes a legal conversation.

Infrastructure right-sizing

‍Canal+’s gains included improved machine sizing, .not over-provisioning “just in case,” but provisioning to the SLO boundary. Google’s tail-latency research notes that tail-tolerant techniques can allow higher utilisation without lengthening the tail, meaning SLO-driven testing often surfaces headroom that naive capacity planning leaves on the table.

Release confidence with teeth

‍Houghton Mifflin Harcourt now runs all 50 of its load simulations together before release, including campaigns at four to five times normal traffic before peak periods. They report fewer performance issues in production as a direct result. That’s what release confidence looks like when it’s backed by data rather than optimism.

Velocity preservation, not velocity reduction

‍This is the counterintuitive point that matters most for CTO-level conversations. Google’s error-budget guidance is explicit: exhausting budget may temporarily slow release cadence, but the purpose is to restore safe release speed, not to punish teams. DORA’s research consistently shows that speed and stability are not structural trade-offs for most organisations. SLO-driven load testing is not anti-delivery; it’s what makes delivery sustainable at scale.

Scaling it: the organizational dximension

The most important lesson from The Home Depot’s SLO program isn’t technical. Before adopting a common SLO framework — covering volume, availability, latency, errors, and tickets — their monitoring was fragmented, root causes were hard to pinpoint, and teams wasted “countless hours” working backwards from user-facing symptoms.

After implementing the framework with training, automation, and executive reporting, they scaled from approximately 50 services reporting SLOs to 800 within a year. Around 50 new services were being onboarded per month. They also integrated SLOs into destructive testing, automatically recording the effect of chaos experiments on service metrics.

That’s not a tooling story. It’s an operating-model story. SLOs gave engineering, SRE, product, and leadership a shared language — and that language made reliability visible, discussable, and governable at scale.

Also Evernote’s experience reinforces the cross-team effect. Working with Google’s CRE team, they adopted an error-budget approach and within nine months were already on version 3 of their SLO practice. Monthly SLO reviews replaced ad hoc outage conversations, and both Evernote and Google had a common, data-driven way to discuss service quality. SLOs improved supplier management and internal prioritisation simultaneously.

Where to start: a practical roadmap

The highest-confidence starting point is narrow scope and high relevance: pick two or three critical user journeys, define SLIs for them, set internal SLOs that are stricter than your SLAs, and encode them as test thresholds.

Then connect those thresholds to runtime telemetry and attach burn-rate alerts and release-gate policies.

A five-phase performance testing maturity model emerges consistently from the literature:

  1. Define: Identify critical user journeys and existing telemetry. Draft SLIs, internal SLOs, and SLA buffer policy.
  2. Instrument: Add percentile histograms, error counters, and saturation metrics to your services.
  3. Automate: Encode SLO thresholds in load tests and CI/CD pipelines. Connect traces, logs, and server-side metrics.
  4. Operate: Run regular SLO reviews. Add fast-burn and slow-burn alerts. Use SLOs for canary releases and peak-readiness drills.
  5. Expand: Roll out to more services and teams. Build executive dashboards alongside service-owner dashboards.

The most common pitfalls are worth naming explicitly: setting 100% SLO targets (which eliminates the error budget entirely), using averages as pass criteria (which hides tail failures), copying another company’s thresholds (which produces governance that doesn’t fit your architecture or user expectations), and treating SLOs as dashboards without consequences (which fails to change engineering prioritisation).

The strategic call to action

The diagnostic question for any CTO is simple: if your load testing program isn’t tied to SLO attainment, error-budget consumption, and release decisions, what decisions is it actually driving?

Canal+ answered that question before a major broadcast and served millions of viewers without a single incident. The Home Depot answered it and scaled reliable service delivery across 800 systems. LoginRadius answered it and halved its production latency.

The technology to do this is mature, well-documented, and largely open-source. The organizational will to tie test outcomes to release decisions and infrastructure investment is the harder part since four in five serious outages are attributed to preventable process failures, not missing technology.

But that’s exactly what separates performance engineering that generates activity from performance engineering that generates governance value.

SLOs don’t make load testing more complicated. They make it more useful.

VS Code Codex “Create Agent” — and Suddenly Your AI Has the IQ of a Goldfish

At first glance it feels magical:

Click button → AI becomes autonomous engineer.

Reality check:

The moment you do this on a real project, you discover something very funny:

The agent has absolutely no idea what your infrastructure looks like.

No conventions. No memory. No project architecture. No operational rules. No understanding of existing tooling. No clue what was already decided three weeks ago.

So even if your repository already contains:

prompts
configs
standards
workflows
APIs
skill systems
deployment logic
output structures
naming conventions
security policies

…the agent still starts asking things like:

“Where should generated files go?”

Brother. There are 40 markdown files explaining that.

The Funny Part

People imagine “AI Agents” like this:

[ GPT ]
↓
Instant Autonomous Company

But in reality it’s more like:

[ GPT ]
↓
Confused Intern With Root Access

And that becomes dangerous surprisingly fast.

Because without infrastructure, the agent starts inventing things.

New folders. New architectures. New APIs. New abstractions. New config formats.

Every five minutes:

“I created a robust scalable foundation…”

No. You created three YAML files and emotional damage.

What Actually Matters

The real product is not the model.

The real product is:

conventions
memory
repository discipline
project instructions
safety boundaries
reproducible workflows
operational structure

The AI is only one component.

Without infrastructure, an agent is basically:

Autocomplete with confidence issues.
What We Do at XvX Systems

Inside XvX we started treating agents less like chatbots and more like workers joining an existing organization.

Meaning:

Every skill has structure.
Every output has a defined location.
Every workflow has routing.
Every system has conventions.
Every AI process is inspectable.
Every automation should be reversible.

A good agent bootstrap should answer things before the agent even asks.

Example:

Use English for code comments.
Do not invent folder structures.
Use existing config files.
Read /prompts first.
Do not modify unrelated files.
Store outputs in user-separated directories.
Avoid destructive commands.

This sounds boring.

But THIS is the infrastructure that turns “AI magic” into something operational.

The Current State of AI Agenting

Honestly?

A lot of current AI agent demos are held together by:

vibes
screenshots
terminal recordings
optimism
caffeine
and one developer screaming internally at 3 AM

And that’s okay.

We’re still early.

But I think the next big shift is not:

“better prompting”

It’s:

operational architecture for AI workers.

The companies that understand this first will build systems that are actually maintainable.

Final Thought

The funniest thing about “Create Agent” was not that it failed.

The funniest thing was realizing:

The AI wasn’t stupid.

It simply had no infrastructure.

And honestly?

Most human companies don’t either. – sadly

get digital first – then try AI, trust me otherwise you do the job twice 🙂

Written by MAGNETiX XvX Systems // Cologne

Infrastructure first. Then intelligence.

CI/CD Strategies: The Cost of Over-Complexity for Indie Hackers

When developing my own products or quickly setting things up with a solo team, my perspective on CI/CD (Continuous Integration/Continuous Deployment) processes is usually quite different. The massive, layered, meticulously thought-out pipelines seen in corporate projects are often an unnecessary burden in the indie hacker world. I believe the primary goal of CI/CD for an indie hacker should be to save me time, free me from repetitive tasks, and enable rapid value creation.

Recently, while updating the infrastructure for a side project where I developed financial calculators, I tried to simplify my CI/CD pipeline as much as possible. I want to share some points I encountered and experienced during this process. My aim is not to build the “best” CI/CD, but to explain how I achieve a sustainable, cheap, and low-maintenance workflow that is “best for me.”

What Should a Simple CI/CD Flow Look Like?

As an indie hacker, my top priority is rapid iteration. Testing an idea, gathering user feedback, and continuously improving the product are vital. Therefore, my CI/CD flow must not slow me down; instead, it should facilitate my work. For me, a simple CI/CD flow should automate critical steps while avoiding unnecessary complexity.

Essentially, I need tests to run automatically when I write my code, followed by the application being built and deployed somewhere. Any layer beyond these steps usually gains significance as the project scale grows or the team size increases. For small-scale projects, consciously leaving some steps that might seem “manual” out of automation can significantly reduce the total cost (both time and money). For instance, while an enterprise ERP system might run dozens of integration tests with every commit, I limit myself to just unit and a few integration tests for my side projects. This gives me both speed and cost advantages.

Fast and Automated Tests

Tests are the heart of CI/CD. I generally expect tests to run immediately when I perform a git push. This is the most effective way to catch errors early, especially in small projects. If tests run slowly, I’ve noticed that I eventually give up running them, and even the quality of my commits declines. This became very clear after an experience in 2018 on a client project. During a refactoring, the tests took over 20 minutes to run, so instead of waiting with every change, I started pushing code that “looked like it worked” locally. The result: tests failed, and we had to roll back. Therefore, fast tests are my primary priority.

My testing strategies generally include:

  • Unit tests: Test application logic at the smallest units, finish in seconds.
  • Integration tests: Test interactions with external dependencies like databases or APIs, slower but still complete in a reasonable time.
  • Linting/Static Analysis: Checks code quality and style, catches errors before the build.

💡 Tip for Fast Tests

Try to run your tests in parallel as much as possible. Tools like pytest-xdist in Python or Jest in JavaScript are very helpful for this. Also, quickly spinning up and tearing down a test database in-memory (e.g., SQLite) or within a Docker container can shorten the time. In an enterprise ERP system, copying the test database used to take 15 minutes; moving it to Docker reduced this to 30 seconds.

Automatic Build and Artifact Management

After tests pass, the application needs to be built and prepared for deployment. Since most of my projects run on Docker containers, this step is usually just a docker build command. For my own side projects, I either build this compiled image directly on my server or, for small projects, push it to a free registry like Docker Hub. While I’ve seen numerous artifact management solutions, from Maven repositories to Nexus, in an enterprise ERP system, this simplicity is sufficient for my own projects. What matters is that the build is repeatable and I can easily revert to a previous version at any time.

# A simple Docker build and push process
# An example I use on my own VPS
#!/bin/bash

APP_VERSION=$(git rev-parse --short HEAD)
REPO_NAME="my-indie-app"
REGISTRY="my.private.registry.com" # or Docker Hub

echo "Building Docker image for version: $APP_VERSION"
docker build -t $REGISTRY/$REPO_NAME:$APP_VERSION .

if [ $? -eq 0 ]; then
    echo "Image built successfully. Pushing to registry..."
    docker push $REGISTRY/$REPO_NAME:$APP_VERSION
    echo "Image pushed. Deleting local image to save space."
    docker rmi $REGISTRY/$REPO_NAME:$APP_VERSION
else
    echo "Docker build failed!"
    exit 1
fi

This simple script provides me with version control and prevents unnecessary space usage. Last month, during a build process, my VPS disk filled up to 100%, and the build failed with an OOM (Out Of Memory) error. I then realized I wasn’t cleaning up old images. By adding this script, I resolved this issue.

The Hidden Costs of Over-Complexity

Falling into the trap of “more automation, better” in CI/CD can be costly for an indie hacker. In the corporate world, the cost of automation is often negligible compared to human resource costs. However, in a solo team, designing, setting up, and maintaining the CI/CD pipeline falls entirely on my shoulders. This directly steals valuable time that I should be dedicating to product development.

Maintenance Burden

A complex CI/CD pipeline can, over time, become a project in itself. Dependency updates, tool version differences, configuration changes – finding the answer to “why did this pipeline break?” can sometimes take hours. I once spent 2 days debugging why the npm install command behaved differently on different runners and caused deployments to fail due to cache issues on a client project. Such time loss is unacceptable for my own projects. Therefore, I strive to build a system with as few moving parts as possible.

  • Dependency Updates: When Node.js or Python versions change, the CI/CD environment also needs to be updated.
  • Tooling Changes: Migrating from Jenkins to GitLab CI means a new learning curve and rewriting existing pipelines.
  • Environmental Factors: External factors like disk space on the build server, network issues, or API limits.

⚠️ The Infinite Debugging Loop

Remember, while automation makes our lives easier, the automation tools themselves are software and can contain bugs. When a problem arises in a complex pipeline, finding whether the source is the code, the test, the build, or the deployment tool can be a real detective job. This can be frustrating and demotivating, especially when you’re alone.

Resource Consumption

CI/CD services are usually billed based on the resources you use (CPU, RAM, storage). Popular services like GitHub Actions, GitLab CI, and CircleCI offer free tiers up to a certain usage limit, but these limits can be quickly exceeded on large projects or with frequent commits. For one of my side projects, when I needed to build for multiple platforms (web, mobile) with every commit, I saw my monthly GitHub Actions bill unexpectedly rise. This can become a significant cost item, especially when your project isn’t yet generating revenue.

Using self-hosted runners on my own VPS has become a method I’ve adopted to control these costs. In an enterprise setting, I’ve even seen monthly electricity bills increase due to high CPU consumption by CI/CD servers. On my own small VPS, I have to use resources efficiently to keep these costs at a minimum. For example, I implement optimizations like intelligently managing build caches or recompiling only changed modules.

Learning Curve and Lock-in

Every new CI/CD tool or technology brings a learning curve. Learning Helm charts for deployment on Kubernetes, implementing GitOps with Argo CD, or integrating CI/CD services from different cloud providers requires a significant time investment for a solo team. This learning process sometimes takes more time than developing the core features of the product.

Furthermore, excessive dependency on a specific CI/CD platform or technology (vendor lock-in) also carries risks. If the platform changes or pricing policies shift, I might have to rewrite the entire pipeline. For my own side projects, I try to minimize such dependencies. I use standard tools as much as possible (Docker, Bash scripts, systemd units) so that if I decide to switch to a different VPS provider one day, I won’t have headaches. In my post about [related: my VPS migration experience], I detailed how painful these migration processes can be.

My Indie CI/CD Choices

With the experience gained over the years, I’ve developed specific choices for CI/CD in my indie projects. These provide me with both speed and cost advantages while minimizing maintenance overhead.

Git Hooks and Simple Scripts

Most of the time, the simplest solutions are the most effective. In my own projects, I achieve local CI using git hooks and simple bash scripts. For example, with the pre-commit hook, I automatically run lint and format operations before pushing my code. This prevents dirty code from entering the repository and allows me to start with a cleaner slate on the remote CI.

# A simple hook example I added to .git/hooks/pre-commit
#!/bin/sh

# Lint staged Python files
echo "Running flake8 on staged Python files..."
git diff --cached --name-only --diff-filter=ACM | grep '.py$' | xargs flake8
if [ $? -ne 0 ]; then
    echo "Flake8 issues found. Please fix them before committing."
    exit 1
fi

# Format staged files (e.g., with Black)
echo "Running black formatter on staged Python files..."
git diff --cached --name-only --diff-filter=ACM | grep '.py$' | xargs black
if [ $? -ne 0 ]; then
    echo "Black formatter failed. Please check."
    exit 1
fi

# Run tests
echo "Running unit tests..."
pytest --ignore=integration_tests/
if [ $? -ne 0 ]; then
    echo "Unit tests failed. Aborting commit."
    exit 1
fi

exit 0

Thanks to this hook, I catch errors at the commit stage and resolve them before sending them to the remote CI. This saves me CI time and prevents faulty commits. I personally saw how valuable such local automations were for improving code quality when developing an enterprise ERP system.

Self-Hosted Runner and VPS Economics

As mentioned earlier, the costs of cloud-based CI/CD services can sometimes increase unexpectedly. Self-hosted runners running on my own VPS allow me to control these costs. Even a small VPS (e.g., 2 CPU, 4GB RAM) is sufficient to run multiple CI/CD jobs in parallel for most indie projects. This is a significant advantage, especially for a cost-conscious indie hacker.

I manage these runners with my own systemd units. I ensure my VPS runs stably by monitoring logs with journald and controlling resources with cgroup limits. Last month, I accidentally put a script into an infinite loop by writing sleep 360, which caused it to be OOM-killed. Thanks to the soft limit of cgroup memory.high, my other services were unaffected, and I quickly identified and resolved the issue. Such small touches increase the overall resilience of the system.

Deployment Strategies: “Rolling Restart” and One-Click Rollback

For indie projects, I generally don’t need large-scale and complex deployment strategies like blue-green or canary. The most practical approach for me is to stop the existing Docker container and start the new version, essentially a simple “rolling restart.” This process usually takes a few seconds, and I accept a brief downtime for the application.

However, things don’t always go as planned. Therefore, having a one-click rollback mechanism is vital. If a problem arises with a new deployment, I must be able to quickly revert to the previous stable version. In my own scripts, I keep the last successful image tag in a variable, allowing me to redeploy this image if necessary.

# Summary of a simple deploy and rollback script
#!/bin/bash

APP_NAME="my-indie-app"
CURRENT_VERSION=$(cat /path/to/app_version.txt)
NEW_VERSION=$1

if [ -z "$NEW_VERSION" ]; then
    echo "Usage: $0 <new_version>"
    exit 1
fi

echo "Deploying $APP_NAME version $NEW_VERSION..."

# Pull the new image
docker pull my.private.registry.com/$APP_NAME:$NEW_VERSION
if [ $? -ne 0 ]; then
    echo "Failed to pull new image. Aborting deploy."
    exit 1
fi

# Stop and remove the current container
docker compose -f /path/to/docker-compose.yml down

# Bring up the new version
sed -i "s/$CURRENT_VERSION/$NEW_VERSION/g" /path/to/docker-compose.yml # Update image tag in docker-compose.yml
docker compose -f /path/to/docker-compose.yml up -d

if [ $? -eq 0 ]; then
    echo "$APP_NAME version $NEW_VERSION deployed successfully."
    echo "$NEW_VERSION" > /path/to/app_version.txt
    echo "Old version was: $CURRENT_VERSION. You can rollback using: $0 $CURRENT_VERSION"
else
    echo "Deployment failed! Rolling back to $CURRENT_VERSION..."
    # Rollback by redeploying the previous version
    sed -i "s/$NEW_VERSION/$CURRENT_VERSION/g" /path/to/docker-compose.yml
    docker compose -f /path/to/docker-compose.yml up -d
    echo "Rolled back to $CURRENT_VERSION."
    exit 1
fi

This script offers me a safe deployment and a quick rollback capability. I recall experiencing stressful monitoring sessions lasting hours after every deployment while working on an internal platform for a bank. With this simple method, I minimize that stress on my own projects.

Monitoring and Feedback: The Essentials

No matter how simple the CI/CD process is, knowing how the application behaves after it’s deployed is critical. I never adopt a “deploy and forget” approach. A simple yet effective monitoring and feedback mechanism helps me detect potential issues early.

Log Tracking and Simple Alerts

Regularly tracking application logs is the first step in troubleshooting. In my projects, I collect logs using journald and search with grep when needed. Even a very simple tail -f /var/log/syslog | grep "ERROR" allows me to see many problems instantly. For more advanced projects, I use lightweight solutions like Promtail and Loki.

I also set up simple alerts for critical errors. For example, while blocking SSH brute-force attempts with tools like fail2ban, I use simple scripts I wrote to send me an email or Telegram message if the application returns a specific error code. When a WAL rotation alarm triggered at 03:14 one night, this simple alert allowed me to intervene early in the morning. Such proactive measures prevent major crises.

Performance Metrics

Monitoring application performance directly impacts user experience. For me, tracking basic metrics like CPU usage, memory consumption, disk I/O, network traffic, and API response times is sufficient. I install and use tools like node_exporter and Prometheus on my own VPS. I also create simple dashboards with Grafana to observe the overall status.

In the backend of one of my side projects, I noticed an inexplicable increase in Redis memory usage. Upon examining the metrics, I realized that a specific dataset was being constantly evicted from Redis and reloaded due to the OOM eviction policy. I resolved this issue by changing the policy from allkeys-lru to volatile-lru. Such observations are only possible with monitoring.

When to Look for More Complex Solutions

Everything I’ve discussed so far is ideal for scenarios where an indie hacker starts alone or with a very small team. However, every project has growth potential. So, when does my current simple CI/CD flow start becoming insufficient, and when should I turn to more complex, enterprise-level solutions?

Team Size and Project Scope

As your team grows, coordination and standardization of processes become more important. When multiple developers are working on different features simultaneously, every commit needs to be integrated and tested automatically. In this situation, a more advanced CI system (e.g., parallel test execution, branch-based pipelines) becomes inevitable. When we had more than 10 developers working on an enterprise ERP system, manual processes were completely paralyzed. An automated, comprehensive CI/CD was our savior back then.

As the project scope expands, different services and microservice architectures may come into play. Having a CI/CD pipeline for each service increases overall complexity but allows each service to be deployed independently. This is a common trade-off encountered in monolith vs. microservice decisions. In my post about [related: Transitioning from Monolith to Microservices], I discussed these decisions in detail.

Regulation and Security Needs

If you operate in regulated industries such as finance, healthcare, or government, your CI/CD processes may need to meet specific security and compliance standards. This means stricter access controls, security scans (SAST/DAST), audit logs, and approval processes. For example, every deployment on an internal bank platform required approval from at least two different people. Such requirements necessitate more enterprise CI/CD platforms beyond simple script-based solutions.

Tracking security vulnerabilities (CVEs), and issues like kernel module blacklisting (e.g., algif_aead, CVE-2026-31431) become important, especially when dealing with sensitive data. Integrating these security checks into the CI/CD pipeline is much more reliable and automated than doing them manually.

Conclusion

My perspective on CI/CD as an indie hacker has always been centered on being “good enough and sustainable.” The massive pipelines seen in corporate projects are often an unnecessary burden for my own small but agile projects. In my experience, simple git hooks, basic bash scripts, and self-hosted runners on my own VPS have allowed me to keep costs low and iterate quickly.

The important thing is to remember that CI/CD is a tool, not a goal. Our aim is to create value rapidly and reach our users. On this path, avoiding any complexity that slows us down or consumes unnecessary resources can be an indie hacker’s greatest advantage. Remember, the best CI/CD is the one that costs you the least time and provides the most benefit.

Dino Inside KDE Dolphin: Towards Person‑Centric (and Reptile‑Centric) Software

Dino in KDE Dolphin

Did you ever contemplate that at some point we’ll never see a movie or read a book in its original form – only the rewritten or re-shot version tailored to our preferences and personality – maximizing engagement? If rare purists reach for the original piece(assuming it exists at all), they will surely find it dull: wrong pace, shallow characters, and too many/few sexual or action scenes for their taste.

How about software heavily tailored to personal data and workflows(ideally in real time)? Apps that look little like their upstream. Apps shaped around past user experience and feedback. Apps that enhance the scenarios that matter, dropping the rest. Apps that cross platform and vendor boundaries. The payoff is immediate: better performance, productivity and privacy; less cognitive load to switch tools.

Apps adapt to people, not vice versa.
This isn’t a new idea.

Last year I ran into the Data-Centric Manifesto in the “Personal Knowledge Graphs” book. The premise is explicit: focus on integrating data from different sources; look at apps as interchangeable views over a data layer; give users the power to compose personalized apps from standard add-ons on their own. A few quotes:

… personal data — emails, contacts, calendar events, files, notes, and more — is no longer fragmented across siloed applications but interconnected in a graph structure.

What is needed is flexible, person-centric ways of achieving interoperability (cohesion), while allowing freedom (autonomy) for choosing and combining applications and services managing personal data.

Applications are allowed to visit the data, perform their magic and express the results of their process back into the data layer.

The authors offer a concrete example: instead of needing to pick a single email client, can I compose my favorite email client out of an inbox, a compose window, and a spam filter?

What we have now, instead, is decades of building app‑centric software: incompatible standards, data captured in silos and fragmentation across vendors. Anything else sounds like a pipe dream, doesn’t it? On the other hand, this idea is captivating and resonates deeply. It can have far-reaching consequences and huge impact across many domains, productivity- and privacy- wise.

Supply and Demand

Users can extend and customize existing functionality to fit their actual workflows instead of googling each time how something can be done in a specific tool. Mix and match features from different vendors. Work on the same data across platform and storage boundaries.

What does it take to make it real?

First, tailoring software to the user needs sounds a lot like core FOSS freedoms. Unfortunately, FOSS has never fully fulfilled its potential – turns out that having sources available is neither a sufficient nor a necessary condition in practice. More important is the overhead of working toward intentionally extensible design from the start, maintaining good docs(which devs rarely write willingly) and keeping a stable API – with some closed-source products admittedly doing better job here.

It’s good to have sources available, to make patches – but something else is missing.

Second, there are many existing solutions: COM/ActiveX components, plugin based architecture of WordPress or Firefox, DOM access with Greasemonkey-like extensions in browsers to run user scripts and modify the web pages on-the-fly, Slack integrations, scripting and Microsoft Office macros. Still, we aren’t there yet.

The roadblocks are well known: vendor lock-in, incompatible and conflicting data formats and API, poor docs and lack of ownership for maintaining and ensuring the quality of end-user app mashups. Feel free to extend the list. One thing stands out: these examples all require careful upfront planning and design.

The supply-and-demand declarative pattern comes to mind here. Consider a host app and a set of add-ons, that do the actual heavy lifting. Declare a spec for each add-on. Describe what an add-on offers(supply), then define constraints, dependencies and possible configurations(demand). Introduce an independent factory-like middleware layer tasked with reconciling supply and demand and generating an exact configuration plan to make the assembly work together. This is basically how package management works in most distros. Turns out, the pattern does not scale well for general software.

I actually worked on a project in the past shaped around modules with specs attached and a constraint solver assembling them into a single bundle. The project was eventually abandoned. It required precise specs with all details spelled out explicitly to work, and that in the end didn’t make it easier to develop.

So I spent a few days on a little experiment, approaching the problem in a different way.

The Dino Experiment

Dino in KDE Dolphin

Tailor your desktop to fit your style and needs – FOSS KDE Desktop promos say. It’s indeed a customizable desktop and as a KDE user I have first-hand experience of the resulting pros and cons.

I wanted to extend the real-world app that does not necessarily aligns with my needs. Typically, I’d look up the API and docs to figure out how to write a plugin.

Not this time.

I started with a generic, app-agnostic spec first, acting as a demand. A generic File Manager, in this case. There I lay out expected workflows, API and hooks, focusing on customization and omitting the rest.

Only after that did I look into docs and sources to reconcile my demand with what the concrete app of my choice actually offers. The mapping is partly manual and partly automated with a prompt: Dear agent, please utilize the available docs, if any, otherwise analyze the codebase itself to map high-level spec concepts to concrete classes and functions and along the way identify existing extension patterns, plugin scaffolding, hooks, and signals/slots. I independently built an add-on strictly against my own demanded ephemeral API.

Finally, with the add-on code and mapping scheme available, I asked the agent to glue both pieces(the host app and the add-on) together locally.

I chose KDE Dolphin: mid size project, ~500 files, ~80K locs.

It has neither published API, nor dev docs as far as I can tell. I’d never drill into internals w/o clear guiding docs in the past. It’s black box for all intents and purposes, despite having sources available.

Well, not really. Turns out the internals are “quietly” documented – the Doxyfile is missing, but the code itself is commented. Still, there is no high level overview to get started.

The add-on I decided to embed for the experiment, is a lil silly Tamagotchi-like game to play with right inside the file manager: drag-and-drop files to satisfy the critter’s needs; if neglected, the critter might become little mischievous and retaliate by messing w/ files(well, make backups more often).

And it worked! The plugin, built against a generic File Manager API, was glued to Dolphin quite nicely based on the provided mapping!

Public Spec Repo

How is it useful?

First, plugins written against the spec and working happily in one tool can be easily adapted to another tool with the same role. Second, there’s also an complementary scenario(largely overlooked): decouple functionality found in one tool as a generic plugin and make it available for others as well. Neat.

In general, this means we are now able to tolerate gaps between supply and demand, between the contract as understood by the client and the actual implementation by the service. That was always a major pain point. The more powerful coding agents become, the wider the gap that can be tolerated.

This opens a possibility of maintaining a public repo of generic specs for common desktop roles: generic mail client, calendar, file manager, and so on. Very similar to FreeDesktop standards or schema.org, in fact. Not just yet another spec that nobody supports, but one paired with non-intrusive mappings that project common roles onto popular concrete specimens in the desktop zoo.

Specs that are usable right away. They require neither a lot of upfront work, nor deep rewrites of existing projects. Projects can be gradually updated over time to better align with the roles they represent.

It can’t replace the powerful, heavyweight products that fully embrace properly designed extensible plugin-based architecture and commit to maintaining stable API and docs. That still is the only way forward to ensure stability, proper QA and manageable security risks(at the cost of upfront work). This also requires plugin writers follow the contract precisely and update their work each time the API breaks.

Consider this a middle ground between a proper plugin-based approach, which is not always an option, and pure vibe coding on the road toward automating and personalizing user workflows and boosting productivity.

[Appendix] The Spec in Detail

The spec is progressive and hierarchical, with the most important items at the top and advanced details hidden below. It does not replace docs; it inverts the perspective to support extensibility and map generic roles to concrete classes, modules and symbols in the code.

We need to know where and when to run an add-on(sort of spacetime coordinates in software context) to modify or extend behavior. This requires capturing both static and dynamic details.

The spec needs to cover at least 4 aspects:

  • UI structure: components, widgets, and their relationship. This is not limited to GUI – the same structure is often reflected in CLI, scripting API and IPC(e.g. D-Bus)

  • Flows

  • Data models(e.g. filesystem API). For example, to show NextCloud remote files instead of local folders, the file manager must be extended at this point.

  • Persistent storage: settings and add-on-specific data

For UI, I ended up with this excerpt:

- Main Window: root node, top-level application windowFolder Viewer: lists directory contents
- Side panels: additional dockable panels
  - Bookmarks /  Shortcuts Panel: lists bookmarked folders
  - Details Panel: shows file or folder metadata
- Action bar: main toolbar with navigation and actions
- Status Bar: shows status and progress

This structure is generic and can be mapped to desktop, mobile and cloud based file managers. Also, it’s not GUI-specific – it can do as a single representation for CLI, scripting API, IPC interaction( e.g. the FreeDesktop file-manager-interface D-Bus API is compatible with this structure as well), or even agent-facing interfaces.

With the help of a coding agent I then analyzed the codebase and mapped the generic File Manager spec to specific Dolphin classes and other symbols. The mapping turned out to be quite clean – Dolphin follows the given blue print rather closely.

A few words on flows: they capture sequences – control flow, app lifecycle, user scenarios. A flow consists of a start, an end, and optional intermediate steps. Typical flows for a file manager are:

- App lifecycle: initialization, finalization
  - Folder flow: enter folder, leave folder`

Flows can be nested just like UI elements or data structures. Formally, an outer flow starts before any inner flow and ends after it.

Think of flows as generalization of event handling, signals/slots(Qt), hooks, pub/sub pattern, coroutines, etc. Explicit reasoning in flows(which is called model checking in formal proofs) helps avoid many bugs where initialization or cleanup does not run on all paths. Event-driven systems often have sloppy, inconsistent flows, which leads to issues like stale state or invalid caches. It’s just plain, old GOTO in different clothing. Harmless flow-related glitches are common – for example, the “Select Background Color” dialog sometimes renders blank until resized(refreshed).

Dolphin does not follow this abstraction cleanly. There is no explicit leave-folder finalization point; it can be emulated by intercepting changeUrl signal and running cleanup for the previous location. The code agent also identified DolphinMainWindow::changeUrl() as the entry point for enter-folder step, but in practice it fires twice per navigation – very common in GUI app(sort of signal chatter).

A minimal Tamagotchi code I built against the abstract File Manager blue print uses enter-folder state to trigger interaction(the critter analyses current folder content and may decide to play a little with the flies..) and the leave-folder event to clean up the mess left behind. Finally, I asked a coding agent to glue it into Dolphin using the mapping – and it worked!

The Hidden Power of useRef in React

1.Introduction

When learning React Hooks, most developers immediately focus on hooks like useState and useEffect. While these Hooks are extremely important, another powerful Hook that often gets overlooked is useRef.

At first glance, useRef may seem confusing because unlike useState, updating a useRef value does not re-render the component. But once you understand how it works, you’ll realize that useRef is one of the most useful Hooks for handling DOM interactions, storing mutable values, and improving performance in React applications.

The useRef Hook allows developers to:

  • Access DOM elements directly

  • Focus input fields

  • Store values between renders

  • Manage timers and intervals

  • Avoid unnecessary re-renders

In modern React applications, useRef is commonly used behind the scenes for things like form handling, video controls, scroll tracking, and performance optimization.

In this blog, we’ll explore what useRef actually is, how it works i
nternally, and where it is used in real-world React projects with practical examples and beginner-friendly explanations.

2.What is useRef in React?

useRef is a React Hook that lets you reference a value that’s not needed for rendering.

In simple terms, useRef gives you a way to:

  • Directly access DOM elements

  • Store mutable values

  • Preserve values between renders

Unlike useState, updating a useRef value does not re-render the component.

3.Why Do We Need useRef?

Sometimes in React, we need to:

  • Focus an input field

  • Access a button directly

  • Store timer IDs

  • Remember previous values

  • Work with DOM elements

This is where useRef becomes useful.

Think of useRef as a special storage box that React remembers between renders.

4.Syntax of useRef

const ref = useRef(initialValue);

Example:

const inputRef = useRef(null);

Here:

  • inputRef → Reference object

  • null → Initial value

5.Understanding How useRef Works

useRef returns an object like this:

{
  current: value
}

Example:

const myRef = useRef(0);

console.log(myRef);

Output:

{
  current: 0
}

The actual value is stored inside:

myRef.current

6.Accessing DOM Elements with useRef

One of the most common use cases.

Example:

import { useRef } from "react";

function App() {
  const inputRef = useRef();

  const focusInput = () => {
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" />

      <button onClick={focusInput}>
        Focus Input
      </button>
    </div>
  );
}

How this works:

  1. inputRef is connected to the input element

  2. React stores the DOM element inside inputRef.current

  3. When button is clicked:

inputRef.current.focus()
  • Input field gets focused

7.useRef vs useState

This is one of the most important concepts.

useState useRef
Causes re-render Does NOT cause re-render
Used for UI updates Used for storing mutable values
Updates visible data Stores values silently

8.Example Difference

Using useState:

const [count, setCount] = useState(0);

Changing state updates the UI.

Using useRef:

const countRef = useRef(0);

Changing:

countRef.current++;

will NOT re-render the component.

9.Storing Previous Values with useRef

useRef is useful for remembering old values.

Example:

import { useEffect, useRef, useState } from "react";

function App() {
  const [count, setCount] = useState(0);
  const previousCount = useRef();

  useEffect(() => {
    previousCount.current = count;
  }, [count]);

  return (
    <div>
      <h1>Current: {count}</h1>
      <h2>Previous: {previousCount.current}</h2>

      <button onClick={() => setCount(count + 1)}>
        Increase
      </button>
    </div>
  );
}

Here:

  • count stores current value

  • previousCount.current stores old value

10.Real-World Use Cases of useRef

You’ll commonly use useRef for:

  • Focusing input fields

  • Accessing DOM elements

  • Managing timers

  • Storing previous values

  • Preventing unnecessary re-renders

  • Video/audio controls

  • Scroll position tracking

11.Simple Analogy for Understanding useRef

Think of useRef like a hidden notebook inside a component.

React remembers the notebook between renders, but changing the notebook does not update the screen.

Example:

useState  → updates UI
useRef    → stores values silently

12.Final Takeaway

useRef is a powerful Hook that allows React developers to directly access DOM elements and store mutable values without causing unnecessary re-renders.

While useState is used for updating the UI, useRef is mainly used for storing values behind the scenes and interacting with the DOM efficiently.

Understanding when to use useRef instead of useState is an important step toward becoming a better React developer because it helps improve both performance and code organization.

Why Your AI Safety Theater Is Killing Innovation: A Product Manager’s Guide to Chaos Capital

Why Your AI Safety Theater Is Killing Innovation: A Product Manager’s Guide to Chaos Capital

My six-year-old daughter doesn’t understand rules yet. She approaches problems with a raw curiosity that cuts through layers of inherited assumptions. Watching her reminded me of something uncomfortable: we’re managing AI products with the same risk-averse playbook we used for shipping physical products in the 1990s, when failure meant recalling inventory and issuing apologies on primetime television.

But here’s what changed: the cost of failure has collapsed to near-zero while we’re still acting like we’re launching space shuttles.

The Evolution Problem

I’ve been thinking about first principles after listening to recent conversations with AI product leaders who are actually shipping. Evolution didn’t optimize for safety. It optimized for rapid iteration with massive failure tolerance. The organisms that survived weren’t the ones with the best guardrails, they were the ones that could adapt fastest to changing conditions.

Yet here we are, building elaborate AI safety frameworks that slow every decision to a crawl. We’re cosplaying risk management while our competitors are learning from real user interactions.

The uncomfortable truth: most AI safety measures aren’t protecting users. They’re protecting product managers from having difficult conversations with executives who still think software deployment works like manufacturing.

The Golden Path Fallacy

The current orthodoxy preaches building guardrails that make the “golden path” the only path. This sounds responsible until you realize it assumes we know what the golden path looks like for AI products that have never existed before.

Traditional product development had physical constraints that forced us to think carefully before acting. You couldn’t easily recall a shipped product or patch hardware bugs with overnight deployments. Those constraints created valuable habits around upfront planning and risk mitigation.

AI development has different constraints. You can roll back a model change instantly. You can A/B test algorithmic behavior with surgical precision. You can implement circuit breakers that activate in milliseconds. The physics have changed, but our management practices haven’t.

Chaos Engineering for AI Products

The most successful AI product teams I’ve observed aren’t the ones with the most elaborate safety procedures. They’re the ones that have architected their systems for controlled chaos from day one.

This means building products that can handle unexpected AI behavior gracefully rather than trying to prevent that behavior entirely. When your AI assistant generates something weird, does your product gracefully degrade or does it brick the entire user experience?

Real AI safety isn’t about preventing AI from doing unexpected things. It’s about building products resilient enough that unexpected AI behavior becomes a feature, not a bug.

The Agency Gap

There’s a growing divide between teams that embrace AI uncertainty and those that try to eliminate it. The difference isn’t skills or resources, it’s agency. Teams that thrive give their people permission to experiment rapidly and take accountability for the results.

The traditional product management framework assumes you can define requirements upfront and measure success against predetermined metrics. AI products demand a different approach: define constraints rather than requirements, measure learning velocity rather than just outcome metrics.

Permission Architecture

The most important job for technical leadership in the AI era isn’t building better guardrails. It’s giving permission and taking accountability. The framework is simple: tell people they can experiment, and if anything goes wrong, blame the system design, not the individual decisions.

This requires rebuilding your product architecture around rapid experimentation rather than predictable execution. Your infrastructure should make it easier to try something new than to maintain the status quo.

The Optimization Trap

We’re optimizing for what we have instead of exploring what’s possible. This is natural human behavior, but it’s particularly dangerous in AI product development where the landscape shifts monthly.

Quality in AI products isn’t about defect rates or Six Sigma metrics. It’s about building something users actually need, which requires constant exploration of the adjacent possible. You can’t explore effectively if every experiment requires a three-week approval process.

Building for Digital Evolution

The teams winning in AI aren’t the ones with the best safety documentation. They’re the ones that have created environments where good ideas can emerge from anywhere and spread quickly through the organization.

This means accepting that most experiments will fail, but optimizing for the speed of those failures rather than their prevention. When failure is cheap and fast, you can afford to be wrong more often. When failure is expensive and slow, you can’t afford to be right.

The companies that will dominate the next decade won’t be the ones that solved AI safety first. They’ll be the ones that learned to harness controlled chaos as a competitive advantage while everyone else was still writing safety documentation.

Your six-year-old doesn’t need permission to be curious. Maybe your AI product team doesn’t either.